@rsc-kit/mcp 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ # Backend-Answered Pages
2
+
3
+ > BAP — the model for an rsc-kit app with a backend behind it, and how to build for it.
4
+
5
+ An rsc-kit app with a backend behind it — Laravel, Go, anything that answers
6
+ the contract — is a **BAP: Backend-Answered Pages**. A page is rendered *in
7
+ front of* the backend rather than *by* it. The renderer asks; the backend
8
+ answers.
9
+
10
+ This page is the whole idea, once. The [Laravel](/hosts/laravel) and
11
+ [Go](/hosts/go) pages are how each backend answers, and
12
+ [Your own backend](/hosts/your-own-backend) is the contract a third one
13
+ implements.
14
+
15
+ ## Beside the shapes you know
16
+
17
+ | | who renders the page | who the backend talks to | how a page gets data |
18
+ | --- | --- | --- | --- |
19
+ | **MPA** — Blade, Rails views, Inertia | the backend | the browser | in the controller, before the view |
20
+ | **SPA** — React + an API | the browser | the browser, over an API | `fetch('/api/orders')` after load |
21
+ | **BAP** — rsc-kit | a renderer, on the server | the renderer, over loopback | `await rpc('Orders.recent')` inside the component |
22
+
23
+ If you have built a SPA with an API behind it, you know most of the shape:
24
+ the React app is what the visitor sees, and the backend is the thing it asks
25
+ for data. The one change is that **the React app is rendered on a server, and
26
+ that server is the one asking the backend** — not the browser. The visitor
27
+ gets a finished, streamed page; the backend never builds one.
28
+
29
+ In plain terms: a restaurant. In an MPA you order from the kitchen and the
30
+ kitchen sends out the whole plate. In a SPA the kitchen hands you an empty
31
+ plate and a recipe, and your browser cooks at the table, running back for
32
+ every ingredient. In a BAP there is a chef between you and the kitchen — the
33
+ renderer. You order a page; the chef plates it right there, asking the kitchen
34
+ through a hatch for what only the kitchen has: your orders, whether you are
35
+ signed in, whether you may be in this room. The kitchen only ever answers. And
36
+ the dishes that never change, the chef plated in advance.
37
+
38
+ ## Why
39
+
40
+ What a BAP gives you that the other two shapes do not:
41
+
42
+ - **No API layer.** A page calls a backend function by name; there is no
43
+ endpoint to design, no serialiser, no client, no versioning of a contract
44
+ between your own frontend and your own backend. The one endpoint that
45
+ exists is private and generic.
46
+ - **The backend keeps everything it is good at.** Sessions, auth, policies,
47
+ validation, queues, mail, migrations — untouched, and reachable from a
48
+ page in one line. A SPA rebuilds half of that in the browser; a BAP never
49
+ moves it.
50
+ - **Nothing ships to the browser for data.** Server components render with
51
+ the data in them. No loading spinners for the first paint, no fetch
52
+ waterfall, no client bundle for the parts that only display.
53
+ - **Streaming and prerendering, on top of a backend.** The shell paints
54
+ before the slow query answers; pages that need nothing per visitor are
55
+ frozen at build time and served as files, and partial prerendering does
56
+ both on one page — none of which an MPA can do, and none of which a SPA
57
+ gets without a second server.
58
+ - **The backend is never reached from the browser.** The host-call
59
+ endpoint is private to the renderer, behind a secret. The attack surface
60
+ is one loopback route rather than an API a visitor can enumerate.
61
+ - **A worker per call, not per page.** The backend answers short calls and
62
+ is free; the renderer holds the long connection. Under a load a Laravel
63
+ MPA feels first in its PHP workers, a BAP feels in the renderer, which is
64
+ the cheaper process to scale.
65
+ - **Typed end to end.** Routes, search params, actions and their inputs are
66
+ typed by the build; a link to a page that does not exist fails
67
+ `tsc`, not the visitor. Backend function names and arguments are next.
68
+ - **Any backend.** Laravel and Go today; the contract is one endpoint, so a
69
+ Rails or .NET app is a small package, not a port.
70
+
71
+ And what it costs, said plainly:
72
+
73
+ - **Two processes.** A JavaScript runtime beside the backend, in
74
+ development and in production. `rsc:install` and the docs make it one
75
+ command; it is still two things to run.
76
+ - **A hop per read.** Loopback, batched, deduped by `cache()`, and gone for
77
+ frozen pages — but a page that reads per visitor asks the backend, and
78
+ under PHP-FPM that is a framework boot per request. Octane makes it a
79
+ millisecond.
80
+ - **A new shape to learn.** The pages are React on the server — server
81
+ components, actions, streaming — which is not the mental model of either
82
+ a Blade view or a React SPA, and the backend's job changes from serving
83
+ pages to answering them.
84
+
85
+ ## How it works
86
+
87
+ ```text
88
+ browser → renderer ──render──▶ React
89
+
90
+ ├─ before rendering: POST /__rsc/host-call { "function": "__rsc.middleware", "args": [["auth"]] }
91
+ └─ during rendering: POST /__rsc/host-call { "calls": [{ "function": "Orders.recent", "args": [5] }, …] }
92
+
93
+
94
+ your backend
95
+ ```
96
+
97
+ - **The renderer is the front door.** It is what `vite dev` serves and what
98
+ Nitro builds into `.output/server`: it routes, renders, serves the pages it
99
+ froze at build time and the assets, and streams the rest.
100
+ - **One endpoint, one direction.** The renderer calls the backend, never the
101
+ reverse, with a shared secret the backend checks on every call. The
102
+ visitor's cookie travels with the call, so the backend's session and auth
103
+ are the visitor's. The backend needs no credential to reach the renderer;
104
+ the renderer's pages are the public site.
105
+ - **Everything else stays the backend's.** Any url the React tree does not
106
+ own — `/login`, a webhook, an OAuth callback, a file under `/storage`, an
107
+ admin panel — is forwarded to the backend as it is. Per url the rule is: if
108
+ the React tree has it, React renders it; otherwise the backend does. Nothing
109
+ is half-and-half on one url.
110
+ - **Calls are batched.** Sibling components each awaiting `rpc()` are one
111
+ request to the backend, answered in order; a guarded page is typically two
112
+ backend requests — the guard, then the batch of its reads.
113
+
114
+ ## What the backend is, then
115
+
116
+ Neither an API nor an MPA. It is the **backend** in the literal sense — the
117
+ part of the application that is not a page: models and the database, sessions
118
+ and auth, policies, validation, queues, mail, events. It does not build pages,
119
+ so it is not an MPA; it does not expose a JSON API to the browser, so it is
120
+ not an API server. It answers one private endpoint that only the renderer can
121
+ call, with functions you write as ordinary classes.
122
+
123
+ For a Laravel app that means: Laravel stops being the app that serves pages
124
+ and becomes the app that answers them. Eloquent, policies, form requests,
125
+ queues, notifications — all of it stays, and none of it is behind a controller
126
+ any more.
127
+
128
+ ## Building for it
129
+
130
+ **Pages read by name.** A server component calls the backend the way it would
131
+ call a function, because from its side it is one:
132
+
133
+ ```tsx title="src/app/orders/page.tsx"
134
+ export default async function Orders() {
135
+ const orders = await rpc<Order[]>('Orders.recent', 5)
136
+
137
+ return <ul>{orders.map((o) => <li key={o.id}>{o.number}</li>)}</ul>
138
+ }
139
+ ```
140
+
141
+ No API to design, no routes to declare, no JSON layer between a component and
142
+ a method. `rpc()` exists in server components during a render and nowhere
143
+ else — the browser never calls the backend directly.
144
+
145
+ **Guards are the backend's, named in `middleware.ts`.** The backend decides
146
+ whether a route may render, in its own vocabulary, and the renderer asks
147
+ before anything at or below that directory renders — including a page frozen
148
+ at build time:
149
+
150
+ ```ts title="src/app/admin/middleware.ts"
151
+ export const middleware = ['auth', 'can:manage-orders']
152
+ ```
153
+
154
+ A refusal is its own kind on the wire — unauthenticated, unauthorized, a
155
+ redirect, a throttle's 429 — and reaches the page as that, never as a 500.
156
+
157
+ **Mutations are server actions the backend implements.** A function the
158
+ backend registers as an action gets a `"use server"` stub written by the
159
+ build, and a client component imports and calls it. The action says what it
160
+ made stale (`Rsc::revalidate('orders')`, `rsckit.Revalidate(ctx, "orders")`)
161
+ and the answer carries the re-rendered region back.
162
+
163
+ **Browser-driven reads are queries that call `rpc()`.** A `query()` is a
164
+ JavaScript server function, so its body can be one call into the backend, and
165
+ the browser reads it over GET with [`fetchQuery`, `usePolling`, TanStack or
166
+ SWR](/guides/queries) on top:
167
+
168
+ ```ts
169
+ export const recentOrders = client.query(async () => rpc<Order[]>('Orders.recent', 5))
170
+ ```
171
+
172
+ **Put things where they belong.** Data, identity, rules and jobs are the
173
+ backend's; layout, interaction and everything a visitor sees is React's. A
174
+ page that needs nothing from the backend is frozen at build time and never
175
+ touches it; one that reads per visitor is a shell with holes the backend
176
+ fills. The build's table says which is which.
177
+
178
+ ## Running it
179
+
180
+ Two processes, and the decision that matters is which faces the internet.
181
+
182
+ **The renderer in front** is the arrangement to reach for: it serves assets
183
+ and frozen pages straight off disk, renders the rest, forwards what it does
184
+ not own, and holds a backend worker for the length of a *host call*, never a
185
+ render. Restrict the host-call endpoint at the web server so only the renderer
186
+ reaches it.
187
+
188
+ **The backend in front** proxies pages to the renderer — the dev-mode shape,
189
+ and what a Laravel app does with `RSC_RENDERER_URL`. It holds a worker for the
190
+ whole render while the render calls back for data, so it needs several
191
+ workers and caps concurrency at one fewer than it has.
192
+
193
+ **What a call costs** is the backend handling a request — under PHP-FPM a
194
+ framework boot, under Octane or a Go process about a millisecond — times the
195
+ number of *sequential* rounds a page needs, which batching keeps to one or
196
+ two.
197
+
198
+ ## When to choose it
199
+
200
+ A BAP earns its second process when the pages want what a server-rendered
201
+ React app gives — streaming, server components with no client bundle, static
202
+ and partially-prerendered pages, typed routes and actions — *and* the data,
203
+ identity and rules already live in a backend you are keeping. An app whose
204
+ backend is only a database is better as a plain rsc-kit app, where the
205
+ "backend" is an import. An app whose pages are simple forms over a Laravel
206
+ model may be happier as an MPA. Everything in between is what this is for.
207
+
208
+ ---
209
+
210
+ Next: [Laravel](/hosts/laravel) · [Go](/hosts/go) · [Your own backend](/hosts/your-own-backend)
package/guides/bun.md ADDED
@@ -0,0 +1,76 @@
1
+ # Running on Bun
2
+
3
+ > What is different when the runtime is Bun — and what only looks like it is.
4
+
5
+ Bun is the default host, and most of an app never notices. What follows is
6
+ what an app *does* notice, collected from real ports: the framework's part
7
+ first, then the things that will look like the framework's fault and are not.
8
+
9
+ ## Vite runs on Bun too
10
+
11
+ `vite` is a bin with a Node shebang, so `bun run dev` alone starts Vite —
12
+ and with it the dev server, the build and the prerender — under **Node**. An
13
+ app that imports `bun`, `bun:sqlite` or a Bun-only driver then fails at the
14
+ first render with `Cannot find package 'bun'`, in a project that just said
15
+ it was a Bun app.
16
+
17
+ The scaffold's scripts run Vite on Bun's runtime, and an existing app should
18
+ too:
19
+
20
+ ```json title="package.json"
21
+ {
22
+ "scripts": {
23
+ "dev": "bun --bun vite",
24
+ "build": "bun --bun vite build",
25
+ "start": "bun .output/server/index.mjs"
26
+ }
27
+ }
28
+ ```
29
+
30
+ `createTestApp()` runs that `build` script, so a test builds on the same
31
+ runtime the server ships on.
32
+
33
+ ## Native dependencies stay outside the bundle
34
+
35
+ A package with a native binary — `sharp`, `bcrypt`, `better-sqlite3`,
36
+ `@prisma/client` — cannot be rolled into a server bundle: the build succeeds
37
+ and the server cannot load its own binary. The usual ones are left external
38
+ by default, and Nitro traces each into `.output/server/node_modules` with its
39
+ binaries, so the deployment is still one directory. For one that is not on
40
+ the list:
41
+
42
+ ```ts title="vite.config.ts"
43
+ rscKit({ serverExternalPackages: ['@acme/native-thing'] })
44
+ ```
45
+
46
+ The same applies on Node; it is only more visible on Bun because the
47
+ scaffold builds there.
48
+
49
+ ## A build machine without the secrets
50
+
51
+ The build prerenders pages, which runs the app, which runs `instrumentation.ts`,
52
+ which imports `env.ts`. A build machine without the production variables
53
+ sets `SKIP_ENV_VALIDATION=1`; the server that runs the build validates at
54
+ startup regardless. And never `NODE_ENV` in a `.env` — Vite honours it, and
55
+ `NODE_ENV=development` turns `vite build` into a build that cannot render;
56
+ [the build refuses it](/installation#environment-variables) and names the
57
+ line.
58
+
59
+ ## Not the framework's, but you will meet them
60
+
61
+ - **`bun test` reads `.env`.** Bun loads the package's `.env` into every test
62
+ run. A test that must not see the app's variables — or must see only the
63
+ ones it sets — runs with `bun test --env-file=/dev/null`.
64
+ - **Stripe's SDK is async-only on Bun.** `stripe.webhooks.constructEvent()`
65
+ throws on every call because Bun has no synchronous WebCrypto;
66
+ `constructEventAsync()` is the same check, awaited.
67
+ - **`pg` puts SQLSTATE in `errno`.** Bun's Postgres driver reports the
68
+ five-character SQLSTATE (`23505`) where Node's `pg` reports it as `code`.
69
+ Match on both if the app runs on both.
70
+ - **Bun's `setTimeout(0)` is a real millisecond**, as Node's is. Nothing to
71
+ do; worth knowing when a test counts ticks.
72
+
73
+ ---
74
+
75
+ Everything else — presets, deployment, the single binary — is on
76
+ [Where it runs](/hosts/where-it-runs).
@@ -44,7 +44,11 @@ it, the server evaluates the library's internals for nothing on every render.
44
44
  | `next/script` | [a `<script>` tag](/guides/third-party-scripts): React 19 hoists and dedupes `async` scripts itself |
45
45
  | `NEXT_PUBLIC_*` | `VITE_*`, read through `import.meta.env`; everything else stays `process.env` on the server |
46
46
  | `next.config.js` | `vite.config.ts` — Tailwind, aliases and plugins are Vite's |
47
+ | `serverExternalPackages` | `rscKit({ serverExternalPackages })`, with the same default list — sharp, bcrypt, prisma and the rest stay outside the bundle; [Running on Bun](/hosts/bun#native-dependencies-stay-outside-the-bundle) |
48
+ | `instrumentation.ts` with `register()` | the same file, in `src/` — imported before any page and awaited before the first request; [startup](/guides/instrumentation) |
47
49
  | `next-safe-action` | `createActionClient()` — same shape, [below](#actions) |
50
+ | `experimental.optimizePackageImports` | on by default for every barrel package the server imports, with no list — read from the barrel itself; `rscKit({ barrelImports: false })` turns it off |
51
+ | a `middleware.ts` rewrite for subdomains | nothing — a host is a route segment, so `acme.example.com/` reaches `app/[domain]/page.tsx` and `admin.example.com/` reaches `app/admin/page.tsx` — [domains](/guides/domains) |
48
52
  | `app/robots.ts`, `app/sitemap.ts` | the same files, the same shapes — [robots, sitemap and llms.txt](/guides/seo-files); `app/llms.ts` beside them |
49
53
  | `@react-email/render` in a server action | the same call, in a module that starts with `"use ssr"` — [emails](/guides/emails). Next fails the same way where it renders server components; the directive is how this one moves it |
50
54
  | `cache` from `react` | `cache` from `@rsc-kit/core/cache` — React's memoises only inside a component render; this one spans the request, so a guard, the layout and the action share one call. The build names server files still importing React's |
@@ -0,0 +1,129 @@
1
+ # Deploying
2
+
3
+ > What to ship, and the two settings that fail quietly.
4
+
5
+ `npm run build` writes one directory, and it is the deployment:
6
+
7
+ ```
8
+ .output/server the server Nitro built, and the engine it calls
9
+ .output/public hashed assets and the pages frozen at build time
10
+ ```
11
+
12
+ ```sh
13
+ npm run build
14
+ npm run start # node or bun .output/server/index.mjs
15
+ ```
16
+
17
+ `npx vite preview` runs the same build through Vite's preview server, which is
18
+ what Nitro suggests at the end of a build. Both serve the real thing; `start`
19
+ is what a deployment runs.
20
+
21
+ ## Anywhere that runs the runtime
22
+
23
+ `.output/` is self-contained. Copy it and start it — a container, a VPS, a
24
+ process manager, a platform that runs a Node or Bun process. There is nothing to
25
+ register and no platform API to satisfy, and **no `node_modules`**: the
26
+ dependencies are in the bundle.
27
+
28
+ ```dockerfile
29
+ FROM oven/bun:1 AS build
30
+ WORKDIR /app
31
+ COPY package.json bun.lock ./
32
+ RUN bun install --frozen-lockfile
33
+ COPY . .
34
+ RUN bun run build
35
+
36
+ FROM oven/bun:1
37
+ WORKDIR /app
38
+ COPY --from=build /app/.output ./.output
39
+ CMD ["bun", ".output/server/index.mjs"]
40
+ ```
41
+
42
+ The second stage carries `.output` and nothing else. On the docs application
43
+ that is 1.0 MB against 104 MB of `node_modules`.
44
+
45
+ ## Or a platform, without a Dockerfile
46
+
47
+ Change the preset and Nitro produces what that platform expects — a Worker and
48
+ its `wrangler.json`, a Vercel function, a Netlify handler. See
49
+ [Where it runs](/hosts/where-it-runs).
50
+
51
+ ## Two things that fail quietly
52
+
53
+ **Do not set `NODE_ENV` when starting the server.** The build bakes its mode
54
+ into the bundle, so a server started with nothing set is production because it
55
+ was *built* that way. Setting it at start time is a second source of truth and
56
+ the one that can disagree — and when it disagrees the failure is silent: every
57
+ page renders, and none of them hydrate.
58
+
59
+ The check is React's debug rows in the payload:
60
+
61
+ ```sh
62
+ curl -s https://your-app.example.com/ | grep -c ':D{'
63
+ ```
64
+
65
+ `0` on a correct production build. Anything else means a development bundle
66
+ reached the client.
67
+
68
+ **Serve `.output/public` at the root.** Nitro does this itself, so this only
69
+ matters behind a CDN: point it at that directory and let the hashed filenames do
70
+ the caching — they are content-addressed, so they can be cached forever.
71
+
72
+ ## Server actions across a deploy
73
+
74
+ A server action can close over server-side values, and React encrypts those
75
+ before sending them to the browser so the page cannot read them. The process
76
+ that decrypts them on the way back has to hold the same key.
77
+
78
+ By default that key is generated at build time and baked in. Every instance
79
+ running the same build agrees, so the only exposure is the deploy itself: a
80
+ browser sitting on a page from the old build calls an action on the new one,
81
+ and the key has changed underneath it. The call fails.
82
+
83
+ For most apps that window is seconds and nobody notices. If yours is long
84
+ enough to care about — a slow rollout, long-lived pages, an app people leave
85
+ open — pin the key:
86
+
87
+ ```sh
88
+ # once, kept wherever you keep secrets
89
+ openssl rand -base64 32
90
+ ```
91
+
92
+ ```sh
93
+ RSC_ACTION_ENCRYPTION_KEY=<that value>
94
+ ```
95
+
96
+ Set it at **build time** and the build stops baking its own; the value is read
97
+ from the environment when the server runs, so the same artifact deploys
98
+ anywhere.
99
+
100
+ :::danger[Set it everywhere or nowhere]
101
+ Half-configured is worse than unconfigured. If one instance reads the variable
102
+ and another falls back to a baked key, they disagree, and the symptom is an
103
+ action that fails for some visitors and not others with nothing in the logs
104
+ pointing at a key.
105
+
106
+ Unset, everything works — the build-time key is used, which is the default
107
+ precisely because it cannot be got half right.
108
+ :::
109
+
110
+ ## Frozen pages and the routes that own them
111
+
112
+ `.output/public` holds whole frozen pages and PPR shells alongside the assets.
113
+ They are read through the `prerendered` reader, which is a function rather than a
114
+ directory precisely so a runtime with no filesystem — a Worker — can supply them
115
+ from a binding instead.
116
+
117
+ A route that declares middleware is never cached publicly: it is sent as
118
+ `private, no-store`, because middleware runs per visitor. If something in front
119
+ of your app also owns the response — an auth proxy re-issuing a session on
120
+ pass-through — give the paths it covers a `middleware.ts` so this host knows
121
+ they are covered. See [serving shells from a CDN](/guides/edge-caching).
122
+
123
+ ## Rebuild on deploy
124
+
125
+ Cached responses carry a build version, so a deploy invalidates them.
126
+
127
+ Ship `.output/` from the same commit as the code that serves it. A server
128
+ running one build against another's frozen pages is the one combination nothing
129
+ checks for you.
@@ -0,0 +1,129 @@
1
+ # Domains and subdomains
2
+
3
+ > A host as a route segment — admin.example.com reaches app/admin, a tenant's host binds [domain] — with nothing to rewrite.
4
+
5
+ Next routes a subdomain with a `middleware.ts` that rewrites
6
+ `acme.example.com/settings` to `/acme/settings` before matching, and the
7
+ route tree never learns a host was involved. Here the same rule is the
8
+ router's own, so the build can see it: typed routes, one stored page per
9
+ tenant, and no middleware to write.
10
+
11
+ ## The rule
12
+
13
+ A request from a host that is not the site's own is matched **with the host
14
+ in front of the path**:
15
+
16
+ | request | matched as | file |
17
+ | --- | --- | --- |
18
+ | `example.com/admin` | `/admin` | `app/admin/page.tsx` |
19
+ | `admin.example.com/` | `/admin` | `app/admin/page.tsx` — the same file |
20
+ | `acme.example.com/settings` | `/acme/settings` | `app/[domain]/settings/page.tsx`, `domain: "acme"` |
21
+ | `acme.com/settings` | `/acme.com/settings` | the same file, `domain: "acme.com"` |
22
+
23
+ A subdomain of the site contributes its label; any other host contributes the
24
+ whole host. The site's own hosts — the one in the root layout's
25
+ `metadataBase`, `www.` of it, and any named in `rscKit({ hosts })` — contribute
26
+ nothing, so the apex keeps path routing and an app adds tenants without moving
27
+ a file. `localhost` and an ip address are always the site's own.
28
+
29
+ Nothing to configure for that: `metadataBase` names the apex, and a
30
+ directory does the rest. `hosts` is for a name that is neither the apex nor a
31
+ subdomain of it and is still the site rather than a tenant — a staging or
32
+ internal name, or a second brand domain:
33
+
34
+ ```ts title="vite.config.ts"
35
+ rscKit({ hosts: ['app.internal', 'example.co.uk'] })
36
+ ```
37
+
38
+ Listing a subdomain there makes it the site by path instead of a tenant —
39
+ the "app on `app.example.com`, marketing on the apex" split — which is a
40
+ choice, not a requirement.
41
+
42
+ And only when a route could answer it: a `[domain]` directory at the top of
43
+ `app/`, or a directory named for the host. An app with a `metadataBase` and no
44
+ tenant tree routes every host by path, and a proxy that forwards to the app by
45
+ an internal name is not read as a tenant called `internal`.
46
+
47
+ The visitor's url is untouched: `acme.example.com/settings` stays in the
48
+ address bar, and a link to `/billing` on that page goes to
49
+ `acme.example.com/billing`. Only the match changed.
50
+
51
+ ## A tenant tree
52
+
53
+ ```tsx title="src/app/[domain]/layout.tsx"
54
+ import { notFound } from '@rsc-kit/core/not-found';
55
+ import { tenantByDomain } from '@/lib/tenants';
56
+
57
+ export default async function TenantLayout({ params, children }) {
58
+ const { domain } = await params;
59
+ const tenant = await tenantByDomain(domain); // "acme" or "acme.com", as stored
60
+
61
+ if (!tenant) notFound();
62
+
63
+ return <TenantProvider tenant={tenant}>{children}</TenantProvider>;
64
+ }
65
+ ```
66
+
67
+ `[domain]` is an ordinary dynamic segment: `params.domain` in every page and
68
+ layout below it, `route('/[domain]/settings', { domain })` typed, `loading.tsx`
69
+ and `error.tsx` where you put them. A directory named for a host,
70
+ `app/admin/`, wins over `[domain]` the way a static segment wins over a
71
+ parameter anywhere else.
72
+
73
+ One difference from a parameter deeper in the tree: a `[domain]` at the top
74
+ of `app/` binds **only from a host**, never from a path. `example.com/nope`
75
+ is a 404, not a tenant called `nope`, and `acme.example.com/` cannot be
76
+ reached as `example.com/acme`. Next has no such guard — its `[domain]` folder
77
+ matches any path once the rewrite is in place — which is why Next apps tuck
78
+ the tenant tree under a route group.
79
+
80
+ ## Domains in a database
81
+
82
+ `generateStaticParams` on the tenant route is the hook. The listed hosts are
83
+ rendered at build and stored, one file per host; a host added afterwards
84
+ falls through to the plain tree, or — if the page reads the request — renders
85
+ on demand and resolves at request time:
86
+
87
+ ```ts title="src/app/[domain]/page.tsx"
88
+ export async function generateStaticParams() {
89
+ const tenants = await db.tenant.findMany({ select: { domain: true } });
90
+
91
+ return tenants.map((t) => ({ domain: t.domain }));
92
+ }
93
+ ```
94
+
95
+ A tenant's page that reads `cookies()` or awaits `connection()` is dynamic
96
+ for that tenant and stored for none, exactly as any page is.
97
+
98
+ ## Locally
99
+
100
+ Keep `metadataBase` as the production host. `localhost` and an ip address
101
+ are always the site's own, so the dev server routes by path as it always
102
+ did, and nothing changes until a `[domain]` directory exists. To try a tenant
103
+ without DNS, send the host the router will see in production:
104
+
105
+ ```bash
106
+ curl -H 'X-Forwarded-Host: acme.example.com' http://localhost:3000/
107
+ ```
108
+
109
+ Or point `acme.example.com` at `127.0.0.1` in `/etc/hosts` and open it on
110
+ the dev server's port in a browser.
111
+
112
+ ## Behind a proxy
113
+
114
+ The host is read from `X-Forwarded-Host` first, then `Host`. A load balancer
115
+ that terminates TLS and forwards to the app by an internal name still routes
116
+ by the name the visitor typed.
117
+
118
+ ## Not for a static export
119
+
120
+ An export is served by a file server, which sees no host. Host routing is a
121
+ server feature; an exported site is the site's own on every host it is
122
+ served from.
123
+
124
+ ## Coming from Next
125
+
126
+ Delete the rewrite in `middleware.ts` and the `[domain]` directory works as
127
+ it did; the segment binds the same value the rewrite put there. Next's
128
+ `rewrite()` for anything else is not here — a host maps to a tree by file,
129
+ not by code.
package/guides/errors.md CHANGED
@@ -7,6 +7,15 @@ nearest one wins.
7
7
 
8
8
  ## When a page throws
9
9
 
10
+ With nothing of your own, a page that throws shows the engine's error page
11
+ where the page was, and the layouts around it stay: in development the
12
+ message and stack, in production "Something went wrong" with the digest to
13
+ search the server log for, and a **Try again** in both. It used to show
14
+ nothing — React unmounted the document on hydration, a black page with the
15
+ cause nowhere near it.
16
+
17
+ That page is the fallback. To show something of your own:
18
+
10
19
  Put an `error.tsx` in the directory you want to cover:
11
20
 
12
21
  ```tsx title="src/app/orders/error.tsx"
@@ -48,7 +57,8 @@ Log the digest where you log the error, and the two line up.
48
57
  ### It does not catch everything
49
58
 
50
59
  - **Errors in the layout above it.** The boundary sits inside that layout, so a
51
- layout that throws needs an `error.tsx` a directory up.
60
+ layout that throws needs an `error.tsx` a directory up. The engine's own
61
+ page is outermost and catches those too.
52
62
  - **The build.** A page that throws every time it renders fails the build
53
63
  rather than shipping a stored error page. The boundary is for a request that
54
64
  goes wrong, not a page that is broken.