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 five demos serving the same blog under Next, TanStack Start, React Router,
SvelteKit and Express share a fronds/ directory that is identical byte for byte.
(demos/nuxt-blog is the flagship and carries more: presenters, collectors, a named
surface.)
What a host does is mount three facades and, if it renders pages, offer the four client
primitives. Everything else — the boot, the validator, 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()) |
fougere new writes these lines into the shell the host's own tool made, so a new app starts from
what create-nuxt, create-next-app, create-vite or sv create writes today — see
fougere new. A host finds the fronds on its own — the nearest directory, up from the app, that holds
fronds/ — so root is written only when they sit elsewhere, the same option for every host.
Three hosts need no package of their own, and that is not an accident: they serve
Web-standard Request/Response, so the facades 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 protocols are served is a fact about the app, declared once beside sources: and
fronds::
// fougere.config.ts
import '@fougere/adapter-rest';
import '@fougere/adapter-graphql';
export default defineFougere({
protocols: { rest: {}, graphql: {} },
});
graphql: {} 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. The import
makes the protocol available and the key serves it — an app that imports neither carries no
schema builder, and naming one it did not import refuses the boot, naming the import.
Absent means not served. The facade 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 facades
Mounting a facade is not the same as publishing an adapter: what you write below puts
the facade in place, and protocols: 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 { call } from '@fougere/app/web';
export const POST = ({ request }: { request: Request }) => call(request);
call, rest and session take a Request and return a
Response. The file you create is the consent: no file, no facade.
Express adds them as middlewares, like express.json() or cors():
import { fougere, call, rest } from '@fougere/app/express';
app.use(fougere()); // the three facades
app.use(call()); // or one at a time — the envelope serves your
app.use('/admin', rest()); // 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: a call names a facade and an operation, and a command on a facade revalidates every mounted query on that facade. Only the state model differs.
The one thing every bundler must be told
A call through a facade names its address as a literal ({ address: 'post' }), so nothing a
minifier renames reaches it. What still reads a class name is the form — useFormFor(Post)
submits to the address named after the entity — and invoke(Post, 'list') on the server. 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.ts
import { withFougere } from '@fougere/next/config';
export default withFougere(nextConfig);
// 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 fronds:. That is the
gradient, and the host is not part of the decision.