Request, forms & sessions
Every page gets the same typed inputs. Read them; don't reshape them by hand.
Three bindings, always present
Every route executes with three reserved top-level bindings that cannot be reassigned:
request— the current HTTP request: method, normalized path, decoded route params, query, headers, cookies, and the parsed body when the route policy declares one.context— host- and deployment-provided facts: the resolved authority, environment values declared as public, session identity, and capability status.data— the entry point for declared data capability operations.
Missing keys are null
Reading a key that is not present yields null, and null propagates through
further lookups. A template that reads request.body.email on a GET route
renders an empty string rather than failing. Use exists(...) and empty(...)
to branch explicitly:
@if (empty(request.query.q)) {
<p>Type a search term.</p>
} @else {
<p>Results for {{= request.query.q }}</p>
}
Conventions that keep templates predictable
- Read, don't reshape. Keep display logic in the page thin. When data
arrives in the wrong shape, use SDA via
@shaperather than nested loops and conditionals. - One canonical name per fact. Don't copy
request.userinto a local and read both; refer to the canonical path. - Error context is public-only. Error handlers never receive secret values, so don't design pages that depend on secrets being present during a 500.
- Body only where declared.
request.bodyis populated only when the route policy setsbody = "json"/"form"/"raw"and the method is allowed.
Sessions
Sessions are volatile and server-held — they are never a durable store.
Set and clear session keys with the @session effect; the framework issues
and rotates the cookie. In production the session lives in replicated host
memory, not on the page node's disk, so nodes stay replaceable.
@call id = auth.callback()
@session user = id.value.subject
@redirect "/dashboard/", status: 303
Forms
Declare a mutating route's methods and body type in the manifest, then read
request.body:
[routes."/subscribe"]
methods = ["POST"]
body = "form"
Cross-origin state-changing requests without a verified token are rejected before the page runs.