Hosts
A host is whatever serves HTTP: Nuxt, Next, TanStack Start, React Router, SvelteKit,
Express. A Frond does not know which one it runs under, and nothing in fronds/ imports
one — the six demos in this repository share a fronds/ directory that is identical byte
for byte.
What a host does is mount three doors and, if it renders pages, offer the four client
primitives. Everything else — the boot, the judge, the REST table, the form contract —
lives in @fougere/app and is the same everywhere.
What each host costs
| Host | Package | What you write |
|---|---|---|
| Nuxt | @fougere/nuxt | one line in nuxt.config |
| Next | @fougere/next | three one-line route files + withFougere() |
| TanStack Start | none | three route files + the Vite plugin |
| React Router | none | three resource routes + the Vite plugin |
| SvelteKit | none | three +server.ts + the Vite plugin |
| Express | none | app.use(fougere()) |
Three hosts need no package of their own, and that is not an accident: they serve
Web-standard Request/Response, so the doors go in unchanged. @fougere/next exists
because Next has next/headers — one import — and Nuxt's module is larger because an h3
event is not a Request.
What an app publishes
Which protocol adapters are served is a fact about the app, declared once beside
db, remotes and auth:
// fougere.config.ts
export default defineFougere({
db: 'sqlite',
adapters: { rest: true, graphql: true },
});
graphql: true is the whole GraphQL setup: the schema is built from the entities the
app already scanned, so there is no builder to construct and no types to register.
@fougere/adapter-graphql, @pothos/core and graphql are imported only when you
declare it — an app that serves none does not carry a schema builder to find that out.
Absent means not served. The door may be mounted — the route file exists, the
middleware is installed — and it will still decline, because the host does not get to
make this decision. serveRest reads the declaration itself, so all six hosts inherit
one answer instead of each spelling it their own way.
The call envelope is not listed there: it is the wire the client primitives use, not a projection an app chooses to publish.
Mounting the doors
Mounting a door is not the same as publishing an adapter: what you write below puts
the door in place, and adapters: decides whether it serves anything.
A Web-standard host — Next, TanStack Start, React Router, SvelteKit — mounts the handlers directly:
// SvelteKit — src/routes/_fougere/call/+server.ts
import { fougereCall } from '@fougere/app/web';
export const POST = ({ request }: { request: Request }) => fougereCall(request);
fougereCall, fougereRest and fougereSession take a Request and return a
Response. The file you create is the consent: no file, no door.
Express adds them as middlewares, like express.json() or cors():
import { fougere, fougereCall, fougereRest } from '@fougere/app/express';
app.use(fougere()); // the three doors
app.use(fougereCall()); // or one at a time — the envelope serves your
app.use('/admin', fougereRest()); // pages, REST is public. Not one decision.
A path Fougere does not serve calls next(), so an app's own routes keep answering
whether they were registered before or after.
Nuxt mounts them itself — its module has an API for that, and adds the composables at the same time.
The client primitives
useQuery, useCommand, useFormFor and useCurrentUser exist per UI framework, not
per host:
@fougere/react— React, whatever renders it@fougere/svelte— Svelte, as stores@fougere/nuxt— Vue, through Nuxt's own data layer
The rules they obey are shared: designation is class + verb, and a command on an entity revalidates every mounted query on that entity. Only the state model differs.
The one thing every bundler must be told
Designation reads the entity's class name, and that name travels — it is the JSON-RPC
method (post.list) and the REST path. A minifier renames classes, so a production build
would designate an entity nobody hosts:
Entity 'j' is not hosted here. Hosted here: post.
You do not have to declare anything for this. @fougere/nuxt and @fougere/vite reserve
the entity names for you — the first from the scan it just ran, the second by reading
fronds/<frond>/entities/. On Next, withFougere() does the same through webpack:
// next.config.mjs
import { withFougere } from '@fougere/next/config';
export default withFougere();
// vite.config.ts — TanStack Start, React Router, SvelteKit
import { fougere } from '@fougere/vite';
export default defineConfig({ plugins: [fougere(), sveltekit()] });
Express needs none of this: it runs your sources, nothing is minified.
Two hosts at once
Nothing stops a Frond from being served by more than one host: run it in its own process
with serve() and let the others reach it through remotes:. That is the
gradient, and the host is not part of the decision.