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
.output/— the Nitro build (pnpm build).- The Frond sources — next to
.output: the scanner reads them fromprocess.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/). Copyingfronds/alone leaves a flat app with no domain and no error — see the boot log below. - A resolvable node_modules for the fronds — those sources import
@fougere/schema,@fougere/core,better-auth… by name.pnpm deploy --prodproduces exactly that closure; hand-picking packages is re-implementing a tracer, don't. - A TS with the compiler JS API (5.9 — TS 7 has none) — dynamically imported by the scanner, invisible to Nitro's tracer.
- Complete lazy packages — Nitro's pnpm trace of
jitiis partial (its babel transform is loaded on demand). Replace it with the real package. - A writable
.data/— the SQLite file and Nuxt Content's index live there. It is the only thing to persist: one volume. - Secrets by env —
AUTH_SECRET(32+ chars), the publicSITE_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.