HTTP routes¶
/rel answers one shape: a query tree, read or written back. Anything that doesn't fit that
shape — logging a guest in, sending a confirmation email, running a multi-step booking
workflow with real side effects — goes through a declared route instead. A route is an
ordinary Postgres function, written in whatever language Postgres supports (plpgsql, sql,
plv8, ...), that you explicitly declare at a path and rel calls directly over HTTP.
Declaring a route¶
A route is declared one of two ways: a COMMENT ON FUNCTION on the function itself, or a
route.<schema>.<function>.* entry in configuration. Either way, the declaration names the
path (chi syntax — {name} placeholders, {name:regexp} constraints) and, optionally, the
accepted HTTP method(s), a default template, and whether the function is stream_upload or
middleware (see File uploads, Templates, and
Middleware below).
The comment form uses HUML (a leading route::) or JSON (a leading route: followed by {)
— pick whichever you reach for:
comment on function hotel.guest_login(jsonb) is 'route: {"path": "/hotel/login", "method": "POST"}';
Or entirely in configuration, with no comment on the function at all:
A config-declared route overrides a comment-declared one on the same function, with a
warning — the function only needs a path; schema and function are inferred from the
config key itself. Two functions colliding on the same anonymized path (see How routes are
matched below) with an overlapping method are both disabled, with
an error naming both; the same path with disjoint methods (one GET, one POST) is fine —
both are registered, dispatched by method. /auth/*, /rel, and anything else rel itself
serves are reserved: a route or middleware can never be declared there, from either source.
create function hotel.guest_login(req jsonb, out resp jsonb, out content jsonb)
returns record language plpgsql as $$
declare
guest_row hotel.guests;
begin
select * into guest_row from hotel.guests
where email = (req->'body')->>'email';
if not found then
raise exception 'Invalid credentials' using errcode = 'RS401';
end if;
resp := jsonb_build_object(
'status', 200,
'jwt', jsonb_build_object('role', 'guest', 'guest_id', guest_row.id)
);
content := jsonb_build_object('id', guest_row.id, 'name', guest_row.first_name);
end;
$$;
comment on function hotel.guest_login(jsonb) is 'route:: path: "/hotel/login", method: "POST"';
By default, any route (or middleware) function can set jwt on its response and thereby
authenticate the caller as any role. Restrict that with http.functions.allowed_auth, scoped
to your actual login functions — see Configuration reference. The rest of
a session's lifecycle (claims, renewal, roles) is covered in
Authentication.
Function prototype¶
- The first argument may be
json/jsonb, in which case it always receives the request object (see Requests and responses). A route needing nothing from the request may omit it entirely. - The second argument may be
byteaorbytea[], enabling uploads directly in Postgres — see File uploads for the full mismatch/sizing rules. Astream_uploadfunction must not declare either. - Any other "IN" argument must be named and typed
text; it receives whatever the correspondingly-named{placeholder}in the path matched. An extratextargument with no matching placeholder — or any argument of another type past this point — disables the function with a discovery-time error.
Returning a single type replies 200 with a mimetype resolved structurally, same as ever:
text → text/plain, json/jsonb → application/json, bytea → application/octet-stream,
and a domain whose name contains / (over bytea or text) → that name as Content-Type —
see Static files ## Returning binary or text content
directly.
For control over the response itself — status, cookies, jwt, a template, deferring to a
static file — declare two trailing OUT columns instead: ..., OUT resp JSON/JSONB, OUT
content <type>) returns record. resp carries every side-effecting field (see Requests and
responses); content behaves like the single-return value above,
except resp.content_type can override its resolved mimetype. This "full-control" shape is
mandatory for a middleware or stream_upload function, and the only way for any function to
set jwt, override status, or render a template.
Middleware¶
A function declared with middleware: true runs ahead of every route (and, for a /-rooted
one, /rel and static files too) whose path it prefixes — the anonymized-path prefix match is
segment by segment, so /api/{tenant} covers /api/{tenant}/orders but not /api2/.... When
more than one middleware applies, they run shortest-prefix-first, then alphabetically by
schema.function, each as one more function call in the request's own transaction.
A middleware function takes the same request shape and the same full-control return shape as any other route. Its response may:
- return nothing (
NULL) — proceed unchanged; - raise an
RSxxxexception — short-circuit exactly like a route's own would; - return a response with none of
status/template/content_type/static_fileset — itscontent(if any) is shallow-merged intorequest.contextfor the next middleware or the route function, later keys winning on conflict; - return a response with any of those four set — terminate the request immediately, sent as-is.
Any middleware, terminating or not, may also set cookies/headers/jwt/jwt_attrs/csp —
merged per-key across the chain and into whatever eventually renders the response, later
values winning. This is what supersedes the old check_session function and
http.static.access.* rules — see Authentication ## Session
lifecycle and Static files ## Restricting access to
part of the tree.
/auth/* is exempt from all middleware, unconditionally — a session-checking middleware there
could make login unrecoverable, since the callback that would restore a valid session is
itself blocked by the check meant to detect the lack of one. /rel itself is a reserved path,
so a middleware can never be declared directly there; gate it by declaring at / (or any
other prefix that still covers it) instead.
Session-checking middleware runs as the request's already-resolved role, unlike
check_session used to (which ran on the primary connection, before the role switch). Grant
EXECUTE on every middleware function to every role that should reach the paths it covers —
including the anonymous role — or those requests fail with an ordinary Postgres
permission-denied 403, the same as any other route call missing a grant.
Errors are just exceptions¶
Raise a Postgres exception with an RSxxx error code and rel turns it directly into that HTTP
status, with the raised message as the body — no separate error-response plumbing to write:
raise exception 'Access denied' using errcode = 'RS401';
raise exception 'Room not found' using errcode = 'RS404';
Any other error surfaces as a 500, unless it's a Postgres error code rel already maps to an
obvious HTTP status (a permission-denied error becomes 403, for instance).
How routes are matched¶
Every {name}/{name:regexp} placeholder in a path is replaced with the literal {} to
produce that path's anonymized form — the key routes are sorted and compared by. Routes sort
longest-anonymized-path-first (so a more specific path wins over a *-style catch-all);
middleware sorts the opposite way, shortest-prefix-first, so an outer gate runs before an
inner one.
In this section¶
- HTTP reference — every function signature and configuration key in this section, in one place.
- Requests and responses — the request/response JSON shapes every route and middleware function sees.
- Rendering HTML with templates — server-side Jet templates, the CSP
nonce for trusted inline scripts, and
extends/blocklayouts. - File uploads — receiving raw bytes, or streaming an upload straight to disk without routing its bytes through Postgres.
- Static files — the root-level static fallback, masking it with a declared route, and returning binary/text content directly from a route — from the filesystem or straight out of a table.
- CORS and CSP — cross-origin access and the response security policy.
- Authentication — sessions, OpenID Connect, SAML, and roles.
See Configuration reference for the full list of http.* settings,
including request size limits and the route.<schema>.<function>.* namespace.