С любовью ко всем разработчикам на свете ❤️
.ctx adds values to a server-only context. The function runs on the server
during a request; what it returns is merged into the running context, and every
later .ctx and the .loader can read it. It's the place to resolve
request-scoped values — the current user, a per-request flag — once, before the
loaders. (A DB client or anything that doesn't depend on the request belongs in
a plain import, not in ctx — the import is stripped from the client bundle
anyway.)
.ctx(async ({ request }) => {
return { me: await getMe({ request }) } // me is now in ctx for every loader below
})
.loader(async ({ ctx }) => {
const ideas = await prisma.idea.findMany({ where: { authorId: ctx.me?.id } })
return { ideas }
})Cut from the client bundle — its body and the imports it uses are removed, so
it never ships to the browser. This holds whether or not the point has
ssr: true. (It runs on the server, during the request, before the loader.)
.ctx is available on every point — root, base,
plugin, page, layout, component,
provider, query, infinite-query,
mutation, and action — while it's still being composed
(before the loader). After you finalize a point, .ctx is gone; calling it on a
finalized or already-loaded point is a type error and throws at runtime.
It runs only on points that issue a server request. An action always
has a server loader, so its .ctx always runs when it executes. A
query, infinite-query, or mutation runs
its .ctx only when it has a server .loader(); a .clientLoader()-only
query/mutation runs in the browser with no endpoint, so — like a loader-less
page — its server-only .ctx never runs. A page or
layout without a loader also makes no request — see
the gotcha below.
What you return is shallow-merged onto the previous context — later keys win:
.ctx(() => ({ x: 1 }))
.ctx(({ ctx }) => ({ y: ctx.x + 1, x: 999 })) // reads x from the previous ctx
// nextCtx = { ...prevCtx, ...returned } => { x: 999, y: 2 }You can stack as many .ctx calls as you like — each sees the context built up
by the ones before it. There is still only one loader per point.
Return undefined (or nothing) and the context is left untouched — useful for a
.ctx that only validates or redirects:
.ctx(({ ctx }) => {
if (!ctx.feature) return // no override
return { extra: load() }
})The callback gets one object:
.ctx(({ ctx, request, set, points }) => {
// ctx — the context accumulated so far (fully typed)
// request — the incoming Request0
// set — response helper for headers / cookies / status
// points — the server-side points collection (e.g. openapi reads it to build its schema)
return {}
})Parsed input / params / search / body / headers / cookies are also
present when the matching schema is declared above this .ctx in the chain.
When the values don't depend on the request, pass an object directly:
.ctx({ tenant: 'acme' }) // same as .ctx(() => ({ tenant: 'acme' }))
.ctx(() => ({ now: Date.now() })) // a function for request-time valuesPassing a function where an object is expected is a type error
(Use ctx(fn) for function values). Returning an array from a .ctx function
is also a type error (Ctx fn should not return array).
Return (or throw) a RedirectTask to redirect — later .ctx calls and the
loader don't run:
.ctx(({ ctx }) => {
if (!ctx.me) return redirect('signIn')
return { me: ctx.me } // narrows me to non-null for the rest of the chain
})Return (or throw) an Error to abort the request with that error:
.ctx(({ ctx }) => {
if (!ctx.me) throw new ErrorPoint0('Only for authorized users', { code: 'UNAUTHORIZED' })
return { me: ctx.me }
})ErrorPoint0 is the framework's default error class; returning it and throwing
it behave the same. You can swap it for your own class of the same-or-wider
shape — AppError in the rest of this page is just such a class. See
Error handling.
By default a .ctx value is reachable only through ctx.x. Pass expose to
also spread the key at the top level of later .ctx and .loader arguments:
.ctx({ x: 1 }, true) // expose every returned key
.loader(({ ctx, x }) => ...) // both ctx.x and bare x are available
.ctx({ x: 1, y: 2 }, ['x']) // expose only x
.loader(({ x }) => ...) // x is here; y is NOT — only ctx.yexpose: true exposes all returned keys; a string array exposes only those.
Exposed keys accumulate across multiple .ctx calls. These keys are reserved
and can't be exposed (it's a type error): request, input, inputRaw,
data, set, execute, ctx.
.ctx runs only when the point issues a server request — that is, only when
it has a loader. A loader-less page or layout makes no
request, so its .ctx never executes.
// This page has no loader → no request → this .ctx never runs.
export const profilePage = root.lets
.page('/profile')
.ctx(({ ctx }) => {
if (!ctx.me) throw new AppError('nope', { code: 'UNAUTHORIZED' }) // NEVER fires
return { me: ctx.me }
})
.page(({ props: { me } }) => <Profile me={me} />)Do not gate access in .ctx alone — gate it in .with, which runs
at render on both server and client, so it fires on the initial SSR render and
on every later client-side navigation. The production pattern pairs a .ctx
(for server loaders) with a .with (for the render):
export const authorizedOnlyPlugin = Point0.lets
.plugin()
.use(mePlugin) // an upstream plugin that puts `me` in ctx and props
.ctx(({ ctx: { me } }) => {
if (!me)
throw new AppError('Only for authorized users', { code: 'UNAUTHORIZED' })
return { me } // narrows me to non-null in ctx
})
.with(({ props: { me } }) => {
if (!me)
return new AppError('Only for authorized users', { code: 'UNAUTHORIZED' })
return { me } // narrows me to non-null in props
})
.plugin()me comes from the upstream mePlugin — props.me doesn't appear on its own.
See .with and Plugin for the full gate.
Unlike .ctx, .with is server-ssr-and-client: cut from the server bundle
when ssr: false (or after a .clientOnly()), kept in the client build always,
and in the server build only when SSR is on. .ctx (cut from the client bundle)
and .with (cut from the server bundle only when SSR is off) cover the two
halves: loaders and render.
.ctx(fn) // function: receives { ctx, request, set, points, ...parsed }
.ctx(object) // plain object, when values don't depend on the request
.ctx(fnOrObject, true) // expose every returned key
.ctx(fnOrObject, ['a', 'b']) // expose only these keysReturns the same point chain with ctx advanced. Chainable.
| Return | Effect |
|---|---|
| a plain object | shallow-merged into ctx (later keys override earlier) |
undefined / void | leaves ctx unchanged |
an Error (returned or thrown) | aborts the request with that error |
a RedirectTask (returned or thrown) | redirects; later .ctx / loader don't run |
| an array | type error — Ctx fn should not return array |
The function may be async; it's awaited. A plain object argument must not be a
function (Use ctx(fn) for function values).
request, input, inputRaw, data, set, execute, ctx. Listing any in
expose is a type error (Forbidden to expose ctx keys: ...).