Configuration
Your code says what a thing is: its data, what it does. It names no process, no provider, no
database and no protocol. Those choices live in one file at the root of the project,
fougere.config.ts, and changing them changes no line under fronds/. That separation is
the gradient: since nothing in a frond says where it runs, moving it is a line of config.
// fougere.config.ts
import '@fougere/adapter-sql';
import '@fougere/adapter-rest';
import { defineFougere } from '@fougere/core';
export default defineFougere({
fronds: { catalog: { remote: 'http://127.0.0.1:4620' } },
ports: { Payment: 'StripePayment' },
sources: { main: { type: 'sqlite', path: '.data/shop.db' } },
protocols: { rest: {} },
});
Leave a key out and you get the default: every frond in the app's process, the only realization of each port, rows in memory (the boot warns), no REST and no GraphQL. The imports at the top make a source type or a protocol exist; naming one that is not imported refuses the boot, naming the import. Every key, and the package that answers it: fougere.config.ts.
Where each frond runs
fronds: gives a frond an address. Start the catalog alone:
fougere dev catalog --port 4620 # in production: fougere build, then fougere serve
and name that address:
fronds: { catalog: { remote: 'http://127.0.0.1:4620' } },
The code that calls the catalog does not change; the call now crosses the network. Remove the line and the catalog runs in the app's process again. The tutorial does it step by step.
What answers a port
When two classes extend Payment, the boot stops until ports: says which one answers. The same
key orders the links that stand in front of it, outside in:
ports: { Payment: process.env.NODE_ENV === 'production' ? 'StripePayment' : 'FakePayment' },
ports: { Payment: ['AuditPayment', 'StripePayment'] },
The handler asks for Payment and never learns which one it got. See Port.
Where rows live
sources: names each place rows live. The entry with no rows: holds every entity the others do
not list:
sources: {
main: { type: 'sqlite', path: '.data/shop.db' },
archive: { type: 'file', path: './archive', rows: ['Order'] }, // import '@fougere/adapter-file'
},
Order is now kept as files under archive/; everything else stays in main. The repository a
handler asks for is the same either way. An entity with one instance — a search engine's address,
some settings — is listed in values: and injected as that value: env: { type: 'env', values: ['Meili'] } reads MEILI_URL into the Meili a constructor asks for. See
SQLite or Postgres and Values.
What the app serves
protocols: says which APIs the app answers on, beside the calls its own pages make:
import '@fougere/adapter-rest';
import '@fougere/adapter-graphql';
protocols: { rest: {}, graphql: {} },
Importing an adapter makes a protocol available; naming it here is what serves it. See REST, OpenAPI, GraphQL.
What stands around fougere serve
fougere serve is the one host where Fougere is the server, so what you would put around a server
of your own goes in server: — srvx middlewares, outside in:
server: { use: [cors({ origin: 'https://app.example.com' })] },
They stand around what the process answers browsers — REST, GraphQL, and any request none of them
claims, such as a /healthz. Under Nuxt, Next, Express or Hono the key is not read: the server and
its middlewares are yours. See Pick a host.
What the app is made of
Besides an address, an entry of fronds: can say two more things — which frond inherits from which,
and which package brings a frond of its own:
fronds: {
shop: {},
cart: { extends: 'shop' }, // cart and pricing inherit shop's code
pricing: { extends: 'shop' },
blog: { remote: 'http://127.0.0.1:4100' }, // answers elsewhere
'@fougere/log': {}, // a package: every process logs its calls
},
Fronds under fronds/ need no entry; the scan finds them. extends says one thing: cart resolves
the services, repositories and ports shop declared. It grants no right to call — cart still
reaches pricing through a facade or a fact, like any neighbour. A frond others inherit from serves
nothing (the boot refuses it otherwise), so nothing can call it or move it, and it stands in every
process that holds one of its children.
The scheme of remote: names what carries the call: http:// and https://, or binding://CATALOG
for a Cloudflare service binding, all three registered by @fougere/transport-http. Another scheme is
a package's to register — see extension points.
Five rungs, and what each costs
Going up a rung never rewrites what you wrote. What each one costs:
| The step | What it buys | What it costs | |
|---|---|---|---|
| 0 | one frond, one process | a running app — no network, no HTTP | nothing |
| 1 | a second frond, in fronds/billing/ | a boundary of ownership, before any network | no reaching into another frond's files (fougere check reports cross-frond-import); a collector lives in the frond that uses it |
| 2 | fronds: { blog: { remote: '…' } } | the frond runs elsewhere, same code | the hop and its JSON; beyond one machine the receiver must know who calls; a named surface answers nothing for a remote frond; one address per frond, so a frond cannot be deployed twice |
| 3 | the frond moves to its own repository | its own team, release cycle and deployment | its contract is copied (fougere sync), the carrier under facts is yours, and readers of a fact deploy before its sender — see A frond from another repository |
| 4 | the frond is not TypeScript | a Rust or Python process answering the same calls | you honour the card and the wire yourself |
After a split
| results | the same shapes on both sides, lists included |
| errors | the same FougereError — code, message, details — rebuilt on arrival |
| an optional input | absent is undefined; JSON omits the key; an explicit null stays null |
| state | the calling app's session reaches the other process, in the signed envelope |
| caller | invocation.caller names the frond that signed — absent when nothing was established |
| an unreachable process | SERVICE_UNAVAILABLE — 503 on REST — and calls resume once it is back |
| the transport | a timeout and retries on the envelope, never an automatic retry of a command |
The split adds a hop: one HTTP request and its JSON, framed and unframed by the transport, after which the receiving side runs the same checks as a local call. What that costs depends on your network and payloads; crossing-cost measures it on a chain of three fronds, and Observe shows it per call.
One recipient per call
Every call has one recipient: fronds: gives each frond one address, and Facade<T> resolves to one
facade. A device behind a firewall still names one gateway; that it opens the connection itself is a
property of the deployment, not another kind of call.
A send with several recipients comes in two kinds, and only one is here:
| Who decides the recipients | ||
|---|---|---|
| a fact — a post was published | nobody: whoever accepts it | Fact |
| a fleet | the sender, naming the set | not here — address it in the data, as emit-fleet does |
A remote frond adds latency and failure modes; Fougere reports them as typed errors and hides
neither. One process stays the reference; fronds: moves a frond when the need is real.