@rsc-kit/mcp 0.14.0 → 0.16.0

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.
Files changed (52) hide show
  1. package/dist/answers.d.ts +7 -0
  2. package/dist/answers.js +28 -0
  3. package/dist/answers.js.map +1 -1
  4. package/dist/bundleGuides.d.ts +27 -0
  5. package/dist/bundleGuides.js +138 -0
  6. package/dist/bundleGuides.js.map +1 -0
  7. package/dist/index.js +21 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/recipes.js +138 -17
  10. package/dist/recipes.js.map +1 -1
  11. package/dist/report.d.ts +13 -0
  12. package/dist/report.js +1 -1
  13. package/dist/report.js.map +1 -1
  14. package/guides/api-routes.md +168 -0
  15. package/guides/authorization.md +288 -0
  16. package/guides/caching.md +57 -0
  17. package/guides/coming-from-next.md +151 -0
  18. package/guides/connection.md +98 -0
  19. package/guides/edge-caching.md +159 -0
  20. package/guides/errors.md +109 -0
  21. package/guides/file-uploads.md +119 -0
  22. package/guides/fonts.md +117 -0
  23. package/guides/forms.md +528 -0
  24. package/guides/getting-started.md +132 -0
  25. package/guides/images.md +83 -0
  26. package/guides/index.json +187 -0
  27. package/guides/installation.md +338 -0
  28. package/guides/introduction.md +119 -0
  29. package/guides/mcp.md +113 -0
  30. package/guides/metadata.md +289 -0
  31. package/guides/navigation.md +84 -0
  32. package/guides/no-javascript.md +76 -0
  33. package/guides/offline.md +215 -0
  34. package/guides/ppr.md +181 -0
  35. package/guides/pwa.md +260 -0
  36. package/guides/queries.md +340 -0
  37. package/guides/quick-start.md +99 -0
  38. package/guides/react-compiler.md +153 -0
  39. package/guides/redirects.md +143 -0
  40. package/guides/response-headers.md +66 -0
  41. package/guides/route-interception.md +206 -0
  42. package/guides/routing.md +458 -0
  43. package/guides/sections.md +74 -0
  44. package/guides/server-actions.md +444 -0
  45. package/guides/static-generation.md +347 -0
  46. package/guides/testing.md +158 -0
  47. package/guides/third-party-scripts.md +105 -0
  48. package/guides/typed-routes.md +139 -0
  49. package/guides/url-validation.md +143 -0
  50. package/guides/validation.md +175 -0
  51. package/guides/view-transitions.md +120 -0
  52. package/package.json +4 -3
@@ -0,0 +1,288 @@
1
+ # Authorization
2
+
3
+ > Protecting pages, server actions and API routes.
4
+
5
+ There are three ways into your app — a page, a server action, an API route —
6
+ and each needs its own check. Guarding one does not guard the others.
7
+
8
+ ## Protect a page
9
+
10
+ Put a `middleware.ts` in the directory you want to protect:
11
+
12
+ ```ts title="src/app/admin/middleware.ts"
13
+ import { redirect } from '@rsc-kit/core/redirect';
14
+ import { currentUser } from '../../auth';
15
+
16
+ export default async function middleware() {
17
+ const user = await currentUser();
18
+
19
+ if (!user?.isAdmin) redirect('/login');
20
+ }
21
+ ```
22
+
23
+ It runs before anything in that directory or below it renders. Return nothing
24
+ to allow; redirect or throw to refuse.
25
+
26
+ Middleware compose up the tree like layouts — outermost first — and **every**
27
+ request runs the whole chain: a full page load, a navigation, a prefetch, a
28
+ revalidation. There is no flag to remember; the file is the declaration.
29
+
30
+ <Aside type="caution" title="Do not put the check in a layout">
31
+ It looks like it works, and it does on a full page load. But a navigation
32
+ tells the server which layouts the browser already has, and the server skips
33
+ them — that is what makes navigation fast. A request can claim to have your
34
+ layout, and then the check never runs.
35
+
36
+ `middleware.ts` is never skipped.
37
+ </Aside>
38
+
39
+ ## Protect a server action
40
+
41
+ An action is a public endpoint. Anyone can call it directly:
42
+
43
+ ```bash
44
+ curl -X POST /_rsc/action \
45
+ -H 'X-RSC-Action: 0339292364be#placeOrder' \
46
+ -H 'X-RSC-Content-Type: text/plain;charset=UTF-8' \
47
+ --data-binary '["a rubber duck"]'
48
+ ```
49
+
50
+ That is not a hole to plug — an action *is* an RPC endpoint and its id is not a
51
+ secret. It means the check goes **inside the action**, not in the component that
52
+ renders the button:
53
+
54
+ ```ts title="src/actions.ts"
55
+ 'use server'
56
+
57
+ import { currentUser } from './auth';
58
+
59
+ export async function placeOrder(item: string) {
60
+ const user = await currentUser();
61
+
62
+ if (!user) throw new Error('Not signed in');
63
+
64
+ // …
65
+ }
66
+ ```
67
+
68
+ <Aside type="caution" title="Middleware does not cover actions">
69
+ Middleware runs when a route renders. An action renders no route, so none of
70
+ them run. Each entry point defends itself.
71
+ </Aside>
72
+
73
+ ### Write the check once
74
+
75
+ One action is fine. Twenty is twenty chances to forget, and the one you forget
76
+ is the one that matters. Put the check on a client and build every action from
77
+ it:
78
+
79
+ ```ts title="src/server/client.ts"
80
+ 'use server'
81
+
82
+ import { createActionClient } from '@rsc-kit/core/action'
83
+
84
+ export const client = createActionClient()
85
+ .use(async ({ next }) => {
86
+ const user = await currentUser()
87
+
88
+ if (!user) throw new ServerAuthenticationError()
89
+
90
+ return next({ ctx: { user } })
91
+ })
92
+ ```
93
+
94
+ ```ts title="src/server/orders.ts"
95
+ 'use server'
96
+
97
+ import { client } from './client'
98
+
99
+ export const placeOrder = client.input(schema).handler(async ({ input, ctx }) =>
100
+ orders.create(ctx.user.id, input),
101
+ )
102
+
103
+ export const listOrders = client.query(async ({ ctx }) => orders.forUser(ctx.user.id))
104
+ ```
105
+
106
+ `ctx.user` is typed and non-null inside the handler, because the only way to get
107
+ there was through the middleware that put it in. **An action cannot be added
108
+ without the check** — not because a rule says so, but because there is no other
109
+ constructor to reach for.
110
+
111
+ `.handler()` makes an action, `.query()` makes a [read](/guides/queries/), and
112
+ both run the same chain. Add a second `.use()` for a role check and it applies
113
+ to everything built from that client:
114
+
115
+ ```ts
116
+ export const admin = client.use(async ({ ctx, next }) => {
117
+ if (!ctx.user.isAdmin) throw new ServerAuthorizationError()
118
+
119
+ return next({ ctx })
120
+ })
121
+ ```
122
+
123
+ Authorise on **identity, not arguments**. `cancelOrder(id)` that trusts the id
124
+ is the whole of an IDOR — the caller chooses the id, so the handler has to check
125
+ the row belongs to `ctx.user`.
126
+
127
+ ## Protect an API route
128
+
129
+ A [route](/guides/api-routes/) runs the same middleware a page in that directory
130
+ would. Put it under a guarded path and it is guarded:
131
+
132
+ ```text
133
+ src/app/admin/
134
+ middleware.ts ← guards everything below
135
+ page.tsx ← guarded
136
+ api/export/route.ts ← guarded too
137
+ ```
138
+
139
+ A refused route answers **401** or **403** rather than redirecting, and names
140
+ the destination in `X-RSC-Redirect` if the middleware wanted one. A `fetch`
141
+ would follow a redirect and hand back a login page as though it were your data.
142
+
143
+ For a route with no middleware above it, check inside the handler:
144
+
145
+ ```ts title="src/app/api/orders/route.ts"
146
+ import { currentUser } from '../../../auth';
147
+
148
+ export async function GET(): Promise<Response> {
149
+ const user = await currentUser();
150
+
151
+ if (!user) return new Response('Unauthorized', { status: 401 });
152
+
153
+ return Response.json(await orders(user.id));
154
+ }
155
+ ```
156
+
157
+ ## Read the session
158
+
159
+ There is no request object. `headers()` and `cookies()` read the one in flight:
160
+
161
+ ```ts title="src/app/[locale]/middleware.ts"
162
+ import { redirect } from '@rsc-kit/core/redirect';
163
+ import { cookies, headers } from '@rsc-kit/core/request';
164
+
165
+ export default async function middleware() {
166
+ const jar = await cookies();
167
+ const locale = jar.get('locale') ?? negotiate((await headers()).get('accept-language'));
168
+
169
+ if (!locale) redirect('/en');
170
+ }
171
+ ```
172
+
173
+ They work anywhere a request is in flight — middleware, server components,
174
+ actions, API routes — so whatever you already use for locale, feature flags or
175
+ tenants works inside a plain async function. `request()` gives you the whole
176
+ `Request` for anything the two do not cover.
177
+
178
+ They are async for a reason worth knowing: at build time there is no request, so
179
+ a read *suspends*. React freezes the shell above it and only the part that
180
+ wanted a header renders per visitor. A synchronous read would force the whole
181
+ page to re-render for everyone.
182
+
183
+ ### Ask once
184
+
185
+ Middleware wants to know who you are; the layout wants their name; the page
186
+ wants their permissions. Wrap the lookup in `cache()` and that is one query:
187
+
188
+ ```ts title="src/session.ts"
189
+ import { cache } from '@rsc-kit/core/cache';
190
+
191
+ export const currentUser = cache(async () => {
192
+ const id = await sessionId();
193
+
194
+ return db.user(id);
195
+ });
196
+ ```
197
+
198
+ The scope is one request. Two requests never see each other's answers, and
199
+ nothing survives between them. Outside a request it just calls through, so
200
+ shared code does not need to know where it is running.
201
+
202
+ ## Set a cookie
203
+
204
+ Middleware runs before the response exists, which makes it the place to put a
205
+ header or a cookie on it:
206
+
207
+ ```ts title="src/app/account/middleware.ts"
208
+ import { cookies, responseHeaders } from '@rsc-kit/core/request'
209
+
210
+ // Middleware runs before anything below it renders, which is also before the
211
+ // host has built a response — so this is the one place left where a header or a
212
+ // cookie can still be put on it. A component runs after, while the response is
213
+ // already streaming, and writing from there throws rather than being dropped.
214
+ //
215
+ // The page below is frozen at build time and stays frozen: what is written here
216
+ // is per request, so neither costs the other anything.
217
+ export default async function middleware() {
218
+ responseHeaders().set('X-Account-Section', 'yes')
219
+
220
+ const jar = await cookies()
221
+
222
+ if (!jar.get('seen-account')) {
223
+ jar.set('seen-account', new Date().toISOString(), { httpOnly: true, sameSite: 'lax' })
224
+ }
225
+ }
226
+ ```
227
+
228
+ Actions can write too, which is the case that matters — signing someone in is a
229
+ mutation that has to leave a cookie behind:
230
+
231
+ ```ts title="src/app/login/actions.ts"
232
+ 'use server';
233
+
234
+ import { action } from '@rsc-kit/core/action';
235
+ import { cookies } from '@rsc-kit/core/request';
236
+
237
+ export const login = action.input(credentials).handler(async ({ input }) => {
238
+ const session = await authenticate(input);
239
+
240
+ (await cookies()).set('session', session.token, {
241
+ httpOnly: true,
242
+ secure: true,
243
+ sameSite: 'lax',
244
+ maxAge: 60 * 60 * 24 * 7,
245
+ });
246
+ });
247
+ ```
248
+
249
+ `get`, `set` and `delete` over the request in flight is the whole surface an
250
+ auth library needs, so you can wire in whichever one you use.
251
+
252
+ <Aside type="caution" title="A component is too late">
253
+ By the time a page renders, the status and headers are already on the wire.
254
+ Writing from a component throws and tells you to move it to middleware —
255
+ rather than accepting the call and quietly dropping it.
256
+ </Aside>
257
+
258
+ ## A guarded page can still be frozen
259
+
260
+ Whether the content is the same for everyone, and whether *you* may see it, are
261
+ different questions. The build answers the first; middleware answers the second,
262
+ per request. So an internal page whose bytes never vary is frozen at build time
263
+ and the middleware decides who gets the file:
264
+
265
+ | Route | Navigation |
266
+ | --- | --- |
267
+ | Guarded and frozen — check, then serve from disk | 6.7 ms |
268
+ | Guarded, rendered on demand | 2626.7 ms |
269
+ | Unguarded, frozen | 0.6 ms |
270
+
271
+ The check runs before the file is read, so a refusal never touches it.
272
+
273
+ <Aside type="caution" title="Two places this does not hold">
274
+ **A static export cannot guard anything.** A static host serves files without
275
+ running code, so exporting a guarded route publishes it to whoever asks.
276
+
277
+ **Do not put a guarded page in a shared cache.** Your origin runs the check; a
278
+ CDN holding the response serves it to the next caller without asking.
279
+ </Aside>
280
+
281
+ ## Where each check goes
282
+
283
+ | Question | Where it goes |
284
+ | --- | --- |
285
+ | May this person see this section? | `middleware.ts` in its directory |
286
+ | May this caller run this action? | Inside the action |
287
+ | May this caller use this endpoint? | Inside the route handler |
288
+ | Should the page show different things to different people? | The page — it renders per request anyway |
@@ -0,0 +1,57 @@
1
+ # Asking once per request
2
+
3
+ > cache() — one lookup, however many places need it.
4
+
5
+ Middleware checks who you are. The layout wants their name. The page wants their
6
+ permissions. That is three calls and one answer.
7
+
8
+ Wrap the lookup:
9
+
10
+ ```ts
11
+ import { cache } from '@rsc-kit/core/cache'
12
+
13
+ export const currentUser = cache(async () => db.user(await sessionId()))
14
+ ```
15
+
16
+ Now call it wherever you need it. The first call runs; the rest get the same
17
+ answer:
18
+
19
+ ```tsx
20
+ export async function middleware() {
21
+ if (!(await currentUser())) redirect('/login')
22
+ }
23
+
24
+ export default async function Page() {
25
+ const user = await currentUser() // already resolved
26
+
27
+ return <h1>Hello {user.name}</h1>
28
+ }
29
+ ```
30
+
31
+ ## One request, and no further
32
+
33
+ Two requests in flight never see each other's answers, and nothing survives into
34
+ the next one. The scope opens when the request arrives and is torn down with it.
35
+
36
+ That is the whole safety story: a table that outlived its request would not be a
37
+ stale cache, it would be one visitor seeing another's data.
38
+
39
+ ## Arguments
40
+
41
+ Compared the way React compares them — primitives by value, objects by identity:
42
+
43
+ ```ts
44
+ const post = cache(async (id: string) => db.post(id))
45
+
46
+ post('a') // runs
47
+ post('a') // reuses the first
48
+ post('b') // runs
49
+ ```
50
+
51
+ Two objects that look the same are two different calls, so pass an id rather
52
+ than an object when you want the reuse.
53
+
54
+ <Aside type="note" title="Not React's cache()">
55
+ Same idea, wider scope. Middleware runs before any component does, which is
56
+ exactly where the duplicate lookups start — and React has no scope open yet.
57
+ </Aside>
@@ -0,0 +1,151 @@
1
+ # Coming from Next.js
2
+
3
+ > What carries over unchanged, what to rename, and what is different on purpose.
4
+
5
+ Most of a Next.js app directory moves over as it is. This page is the rest,
6
+ in the order a port meets it.
7
+
8
+ ## What is the same
9
+
10
+ The `app/` conventions are the same conventions: `layout.tsx`, `page.tsx`,
11
+ `loading.tsx`, `error.tsx`, `not-found.tsx`, `route.ts`, `[slug]`,
12
+ `[...path]`, `(group)`, `@slot` and `(.)intercept`. `"use client"` and
13
+ `"use server"` mean what they mean in React. `cookies()` and `headers()` are
14
+ async and have the same names. `redirect()`, `notFound()`,
15
+ `generateStaticParams()` and `export const metadata` all exist. Streaming
16
+ through `<Suspense>` works the same way, because it is React doing it.
17
+
18
+ Copy `src/app` across first and fix imports second. Most files need only the
19
+ imports.
20
+
21
+ ## Imports
22
+
23
+ | Next.js | here |
24
+ | --- | --- |
25
+ | `next/link` | `@rsc-kit/core/Link` — `href` is typed to your routes, and `search` to the page's schema |
26
+ | `useRouter().push(url)` | `visit(url)` from `@rsc-kit/core/router`; `replace: true` for `.replace()` |
27
+ | `useRouter().refresh()` | `refresh()` from `@rsc-kit/core/router` — or better, `revalidate()` from the action |
28
+ | `usePathname()` | `@rsc-kit/core/usePathname` |
29
+ | `useSearchParams()` | `@rsc-kit/core/useSearchParams`, or [nuqs](/guides/routing#search-params-as-state) with our adapter |
30
+ | `useParams()` | the page's `params` prop, passed down |
31
+ | `cookies()`, `headers()` from `next/headers` | the same names from `@rsc-kit/core/request` |
32
+ | `redirect()` from `next/navigation` | `@rsc-kit/core/redirect` |
33
+ | `notFound()` | `@rsc-kit/core/not-found` |
34
+ | `revalidatePath()`, `revalidateTag()` | `revalidate('tag')` from `@rsc-kit/core/revalidate` — see below, it is narrower |
35
+ | `Metadata` from `next` | `@rsc-kit/core/metadata` — `metadataBase`, `openGraph`, `twitter`, `icons` as you had them |
36
+ | `next/font` | [Fontsource](/guides/fonts): install the font, import its CSS |
37
+ | `next/image` | [unpic](/guides/images) for a CDN, `vite-imagetools` for files in the repo |
38
+ | `next/script` | [a `<script>` tag](/guides/third-party-scripts): React 19 hoists and dedupes `async` scripts itself |
39
+ | `NEXT_PUBLIC_*` | `VITE_*`, read through `import.meta.env`; everything else stays `process.env` on the server |
40
+ | `next.config.js` | `vite.config.ts` — Tailwind, aliases and plugins are Vite's |
41
+ | `next-safe-action` | `createActionClient()` — same shape, [below](#actions) |
42
+
43
+ ## Different on purpose
44
+
45
+ ### Nothing is dynamic by declaration
46
+
47
+ There is no `export const dynamic = 'force-dynamic'` and no `revalidate = 60`.
48
+ A page is frozen at build time unless it **reads the request** — `cookies()`,
49
+ `headers()`, `searchParams`, `await connection()` — and then it renders per
50
+ request, or as a shell with the reading part streamed in. The build prints
51
+ which, and why:
52
+
53
+ ```
54
+ ○ /about no js
55
+ ◐ /orders 85 kB
56
+ dynamic — called cookies()
57
+ ```
58
+
59
+ `await connection()` is the one explicit mark, for a page that must render per
60
+ visitor and does not happen to read anything. Time-based ISR does not exist:
61
+ a frozen page changes when you build, a dynamic one on every request, and
62
+ [edge caching](/guides/edge-caching) covers the middle.
63
+
64
+ ### Middleware is per directory, not one file at the edge
65
+
66
+ Next has one `middleware.ts` that runs on a matcher, on the edge runtime, with
67
+ a restricted API. Here a `middleware.ts` sits in the directory it protects,
68
+ runs on the server with the full API, and covers everything below it:
69
+
70
+ ```ts title="src/app/admin/middleware.ts"
71
+ export default async function middleware() {
72
+ if (!(await currentUser())?.isAdmin) redirect('/login');
73
+ }
74
+ ```
75
+
76
+ It does not run for actions — an action renders no route — which is why the
77
+ check for an action belongs in the action. See [Authorization](/guides/authorization).
78
+
79
+ ### Actions
80
+
81
+ `next-safe-action` users will find the same shape under a different name.
82
+ `createActionClient()` chains middleware, validates with any Standard Schema,
83
+ and **returns** failures as `{ validationErrors }` or `{ serverError }` rather
84
+ than throwing them across the wire:
85
+
86
+ ```ts
87
+ export const client = createActionClient().use(async ({ next }) => {
88
+ const user = await currentUser();
89
+ if (!user) throw new ServerAuthenticationError();
90
+ return next({ ctx: { user } });
91
+ });
92
+
93
+ export const createPost = client.input(schema).handler(async ({ input, ctx, fieldErrors }) => {
94
+ if (await slugTaken(input.slug)) return fieldErrors({ slug: 'Already taken' });
95
+ return save(input, ctx.user);
96
+ });
97
+ ```
98
+
99
+ `returnValidationErrors(schema, { email: { _errors: [...] } })` becomes
100
+ `return fieldErrors({ email: 'Account not found' })`. `useAction` from
101
+ next-safe-action is `useActionState`, or a `<Form action={createPost}>` that
102
+ reads the returned errors on its own. The build lists any action not built
103
+ from a client, because nothing checks who calls those.
104
+
105
+ ### Revalidation is targeted
106
+
107
+ `revalidatePath('/orders')` re-renders the page. `revalidate('orders')`
108
+ re-renders the [section](/guides/sections) registered under that name and
109
+ sends it back **with the action's own response** — one request, the rest of
110
+ the page untouched, a half-typed input elsewhere on it still typed. Wrap the
111
+ region in `section('orders', Orders)` and name it from the action.
112
+
113
+ ### Forms
114
+
115
+ `<Form>` from `@rsc-kit/core/form` submits to an action, shows pending state,
116
+ places field errors, and works before hydration. It is uncontrolled by
117
+ default like React Hook Form's `register`, with `field()` for a controlled
118
+ binding and `useField()` for a value read anywhere. shadcn's `Field`
119
+ components fit as they are. See [Forms](/guides/forms).
120
+
121
+ ### Query strings are typed
122
+
123
+ Export a schema beside the page and the values arrive parsed; the same schema
124
+ types every `<Link search={…}>` to it. `Number(searchParams.get('page'))` is
125
+ not a thing you write here. See [URL validation](/guides/url-validation).
126
+
127
+ ### There is no image optimizer, and no `opengraph-image.tsx`
128
+
129
+ Both are processes Next runs for you at request time. Put `opengraph-image.png`
130
+ in `src/app` and it is picked up; generate one at build time if it has to be
131
+ generated. Images: [unpic or imagetools](/guides/images).
132
+
133
+ ### Testing does not need a browser
134
+
135
+ `createTestApp()` hands back the deployed `Request → Response` handler.
136
+ Actions, queries and api routes are plain functions. There is no equivalent
137
+ in Next; see [Testing](/guides/testing).
138
+
139
+ ## The porting order that worked
140
+
141
+ 1. `bun create rsc-kit@latest` and copy `src/app` over the scaffold's.
142
+ 2. Fix imports from the table. `bun run typecheck` finds the rest.
143
+ 3. `bun run build` and **read the output**: every route that is not `○` says
144
+ why. Most surprises are a `cookies()` in a layout making everything
145
+ dynamic — the build says so under the summary.
146
+ 4. Actions not built from a client are listed. Decide for each.
147
+ 5. `bun run check`. Then a browser, for the parts that are a browser's.
148
+
149
+ An agent doing the port has all of this: the `.mcp.json` in the scaffold
150
+ answers `how_to({ topic })` and `read_guide({ slug })` from the installed
151
+ version, and the build report is what it reads instead of guessing.
@@ -0,0 +1,98 @@
1
+ # Rendering per request
2
+
3
+ > Marking work that belongs to the visitor, not to the build.
4
+
5
+ Most pages can be rendered once, at build time, and served as files. Some work
6
+ cannot: a query whose database the build machine cannot reach, or a value that
7
+ has to differ per visitor.
8
+
9
+ `connection()` marks that work.
10
+
11
+ ```tsx title="src/app/orders/page.tsx"
12
+ import { connection } from '@rsc-kit/core/request'
13
+
14
+ export default async function OrdersPage() {
15
+ await connection()
16
+
17
+ const rows = await db.query('select * from orders')
18
+
19
+ return <ul>{rows.map((r) => <li key={r.id}>{r.reference}</li>)}</ul>
20
+ }
21
+ ```
22
+
23
+ At build time it never resolves, so **nothing after it runs** — the query is not
24
+ made, and the build needs no database. At request time it resolves immediately
25
+ and the component runs normally.
26
+
27
+ ## Call it once
28
+
29
+ This is the mistake worth avoiding:
30
+
31
+ ```tsx
32
+ await connection()
33
+
34
+ const orders = await db.orders()
35
+ await connection() // ← does nothing
36
+ const customers = await db.customers()
37
+ await connection() // ← does nothing
38
+ ```
39
+
40
+ It is a **barrier, not a wrapper**. Everything after it in that component
41
+ belongs to the request, however many calls that turns out to be. The second and
42
+ third calls are already covered by the first.
43
+
44
+ One call, as early as the work begins. That is the whole API.
45
+
46
+ ## It needs a boundary above it
47
+
48
+ The build stores what it *can* paint. If `connection()` is reached before
49
+ anything has painted, there is nothing to store:
50
+
51
+ ```tsx
52
+ export default async function Page() {
53
+ await connection() // nothing above it has rendered
54
+
55
+ return <h1>{await something()}</h1>
56
+ }
57
+ ```
58
+
59
+ Give it a boundary and the page becomes a shell — the layout and headings are
60
+ stored, and the marked part arrives per request:
61
+
62
+ ```tsx
63
+ export default function Page() {
64
+ return (
65
+ <>
66
+ <h1>Orders</h1>
67
+ <Suspense fallback={<p>Loading orders…</p>}>
68
+ <Orders /> {/* calls connection() inside */}
69
+ </Suspense>
70
+ </>
71
+ )
72
+ }
73
+ ```
74
+
75
+ A `loading.tsx` beside the page does the same thing for everything below it.
76
+ Without either, the build refuses the route rather than storing a blank page,
77
+ and tells you which one to add.
78
+
79
+ ## When you do not need it
80
+
81
+ A query the build **can** run, whose answer is the same for every visitor, wants
82
+ none of this. Let it freeze — that is the whole benefit of prerendering, and
83
+ marking it per-request gives every visitor a render they did not need.
84
+
85
+ Use it when the build genuinely should not do the work:
86
+
87
+ - the database or API is not reachable from the build machine
88
+ - the value must differ per visitor
89
+ - the data changes faster than you deploy
90
+
91
+ ## Same as Next.js
92
+
93
+ Same name and same behaviour as Next's `connection()`, so there is nothing new
94
+ to learn if you have used it.
95
+
96
+ You will reach for it less often here, though. The build classifies routes by
97
+ rendering them rather than asking you to declare, so most dynamic pages are
98
+ already understood without a marker.