Deployment

A Fougere app is a Nitro app plus one thing Nitro doesn't know about: the Frond sources are read at runtime (scan, AST parse, jiti load). This page is what deploying this very site taught — the checklist, then the Docker realization.

The runtime checklist

  1. .output/ — the Nitro build (pnpm build).
  2. The Frond sources — next to .output: the scanner reads them from process.cwd() at boot. fronds/**/*.ts, and when the root itself is the Frond, the convention directories at the root (entities/, handlers/, presenters/, collectors/, seeds/, services/, repositories/). Copying fronds/ alone leaves a flat app with no domain and no error — see the boot log below.
  3. A resolvable node_modules for the fronds — those sources import @fougere/schema, @fougere/core, better-auth… by name. pnpm deploy --prod produces exactly that closure; hand-picking packages is re-implementing a tracer, don't.
  4. A TS with the compiler JS API (5.9 — TS 7 has none) — dynamically imported by the scanner, invisible to Nitro's tracer.
  5. Complete lazy packages — Nitro's pnpm trace of jiti is partial (its babel transform is loaded on demand). Replace it with the real package.
  6. A writable .data/ — the SQLite file and Nuxt Content's index live there. It is the only thing to persist: one volume.
  7. Secrets by envAUTH_SECRET (32+ chars), the public SITE_URL.

The verification signal is the boot log — not a 200 on pages (SSR renders happily with zero fronds loaded):

INF [boot:app] scanned 2 frond(s) in 467ms      ← this line
{"jsonrpc":"2.0","id":1,"result":[]}            ← and one envelope call

Docker

The site ships a working pair — site/Dockerfile + site/docker-compose.yml:

SITE_AUTH_SECRET='…32+ chars…' SITE_URL='https://your-domain' \
  docker compose -f site/docker-compose.yml up -d --build

The Dockerfile realizes the checklist in two stages:

# build stage: install workspace → build packages → build site
RUN pnpm --filter "./packages/**" -r run build \
 && pnpm --filter "@fougere/site" run build
# the frond-resolution closure, by pnpm itself:
RUN pnpm --filter "@fougere/site" deploy --prod --legacy /deploy
# complete jiti's partial trace + add TS 5.9

# run stage: node:22-slim, non-root
COPY --from=build /repo/site/.output ./.output
COPY --from=build /repo/site/fronds  ./fronds
COPY --from=build /deploy/node_modules ./node_modules
VOLUME /app/.data
CMD ["node", ".output/server/index.mjs"]

State lives in the site-data volume mounted at /app/.data — backups are one file copy.

Splitting in production

The same image model applies to a split: the host process serving a Frond needs items 1–5 for its fronds, and the consumer declares it in remotes. A dead host degrades to typed 503s in pages (The gradient); restarting it is recovery — the consumer is never rebuilt.

Three things get called "together" and only one of them is required. The contract must travel — the consumer has to know what the host serves. The code need not: fougere sync reads the host's identity card and rebuilds a live schema locally, which is how a Frond written in another language is consumed at all. The ports must reach each other, and that is the only one a deployment decides.

The receiver binds loopback unless told otherwise, so the simplest split puts both processes on one machine: the same pod, or one image. hosts: ['0.0.0.0'] widens it in one line — and hands the operator what the loopback default was holding: a receiver trusts the identity it is handed, so reaching it across a network means closing that path some other way (a mesh, a firewall, a sidecar) until identity is settled at the Frond.

These steps are candidates for a future fougere build — a user should not have to discover them. Until then, this page is the contract.

Built with Fougere — this site runs on the framework it documents.