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 stepWhat it buysWhat it costs
0one frond, one processa running app — no network, no HTTPnothing
1a second frond, in fronds/billing/a boundary of ownership, before any networkno reaching into another frond's files (fougere check reports cross-frond-import); a collector lives in the frond that uses it
2fronds: { blog: { remote: '…' } }the frond runs elsewhere, same codethe 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
3the frond moves to its own repositoryits own team, release cycle and deploymentits 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
4the frond is not TypeScripta Rust or Python process answering the same callsyou honour the card and the wire yourself

After a split

resultsthe same shapes on both sides, lists included
errorsthe same FougereError — code, message, details — rebuilt on arrival
an optional inputabsent is undefined; JSON omits the key; an explicit null stays null
statethe calling app's session reaches the other process, in the signed envelope
callerinvocation.caller names the frond that signed — absent when nothing was established
an unreachable processSERVICE_UNAVAILABLE — 503 on REST — and calls resume once it is back
the transporta 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 publishednobody: whoever accepts itFact
a fleetthe sender, naming the setnot 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.

Put it to work

Built with Fougere and Nuxt, hosted on GitHub Pages.