Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Routing config schema

The routing section of project.cfg is the deploy-scoped config tier. It is authored in RON, parsed at sync, and folded into the immutable deployment manifest — so it is atomic with the content and rolls back with it. Every field is optional; an empty routing: () is all defaults.

Validate it without publishing:

boatramp validate
project.cfg: routing OK (2 redirects, 1 handler)

Top-level fields

FieldTypeDefaultDescription
versionu321Schema version, pinned at 1.
indexlist<string>["index.html"]Directory-index candidates, tried in order.
clean_urlsboolfalseMap extensionless URLs to .html (/about/about.html).
case_insensitiveboolfalseMatch paths case-insensitively against redirects, rewrites, and files.
trailing_slashenumPreserveTrailing-slash policy — see below.
error_documentsmap<u16, string>{}Status code → error document (404: "/404.html").
redirectslist<Redirect>[]Redirect rules, first match wins.
rewriteslist<Rewrite>[]Internal-rewrite or reverse-proxy rules, first match wins.
headerslist<HeaderRule>[]Response-header rules; every matching rule applies, in order.
cacheCacheConfigDefault Cache-Control — see below.
mime_overridesmap<string, string>{}Extension → MIME override (".webmanifest": "...").
proxy_allowlist<string>[]Allowed upstream hosts for proxy rewrites — see below.
handlerslist<HandlerConfig>[]WebAssembly request handlers, matched after redirects, before static lookup.
consumerslist<ConsumerConfig>[]Message-consumer components, invoked per message on a topic.
cronslist<CronConfig>[]Scheduled handler invocations.
streamslist<StreamConfig>[]Host-level SSE / WebSocket endpoints fanning out topics.

Pattern fields (from, matches, handler route) use the path matcher syntax and are compiled at validate/sync, so a bad pattern fails at deploy time rather than at request time.

trailing_slash

ValueEffect
PreserveLeave the path as-is (default).
AlwaysRedirect to add a trailing slash.
NeverRedirect to strip a trailing slash.

redirects

Each rule redirects a matching path. First match wins.

FieldTypeDefaultDescription
frompatternSource path pattern.
tostringDestination, with :name / :splat substitution.
statusu16308HTTP status. 308 is permanent and method-preserving.
whenstringOptional condition — the rule fires only if it and from both match.
redirects: [ (from: "/old/:slug", to: "/new/:slug", status: 301) ],

rewrites

A rewrite serves a different resource without changing the URL. An internal to (a path) rewrites; an absolute-URL to reverse-proxies to that upstream. First match wins.

FieldTypeDefaultDescription
frompatternSource path pattern.
tostringInternal path or absolute proxy URL, with :name / :splat substitution.
statusu16200Status served for an internal rewrite (e.g. 200 for SPA fallback).
whenstringOptional condition — the rule fires only if it and from both match.

An SPA fallback is a rewrite of everything to the app shell:

rewrites: [ (from: "/*", to: "/index.html", status: 200) ],

Proxy rewrites are constrained by proxy_allow.

Conditional rules (when)

A redirect or rewrite may carry a when condition — a small server-side expression over the request. The rule fires only when its from pattern matches and its when is true; otherwise the router keeps looking. This is how you do language- or file-aware routing without a WASM handler, and it runs in the routing hot path (compiled once at sync, then a fast in-memory evaluation per request).

routing: (
  redirects: [
    // Send the root to the visitor's preferred locale.
    ( from: "/", to: "/fr/", status: 302, when: "prefers_language(['fr','en']) == 'fr'" ),
    ( from: "/", to: "/en/", status: 302, when: "prefers_language(['en','fr']) == 'en'" ),
    // Fall back to the English page when a localized file is missing in this deploy.
    ( from: "/fr/*", to: "/en/:splat", status: 302, when: "!file_exists(path)" ),
  ],
)

The expression language is a subset of CEL — boolean expressions only, no loops, no timestamps, no regex — so it is bounded and cheap. It is compiled and type-checked at boatramp validate / sync (a bad expression fails the deploy).

Variables (strings): method, host, path (the normalized request path).

Functions:

CallResultNotes
header("name")stringRequest header value ("" if absent). Name must be a literal.
cookie("name")stringCookie value ("" if absent).
query("name")stringQuery-string value ("" if absent).
file_exists("/path")boolDoes that path serve a file in this deployment (honors clean-URLs + index)?
accepts_language("fr")boolDoes Accept-Language accept the tag (primary-subtag match)?
prefers_language(["fr","en"])stringThe first listed tag the request accepts, else "".

Operators: == != in && || !, string concatenation with +, and the string/list methods .startsWith(…), .endsWith(…), .contains(…).

when: "method == 'GET' && header('X-Country') == 'IT' && path.startsWith('/shop')"

Computed destinations (${…})

A to destination may embed ${<expr>} — a string-valued expression from the same language — so one rule can route to a computed target. ${…} interpolation runs before the usual :name / :splat capture expansion. The classic use is sending a visitor to their negotiated locale in a single rule:

redirects: [
  ( from: "/",
    to: "/${prefers_language(['fr','en','de'])}/",
    status: 302,
    // Only redirect when a supported locale is actually accepted (else "//").
    when: "prefers_language(['fr','en','de']) != ''" ),
],

Embedded expressions are type-checked at validate/sync (each must be a string), and — like conditions — a ${…} that reads a header/cookie/Accept-Language contributes to the response Vary.

Caching. A condition that reads Accept-Language, a cookie, or a header makes the response depend on that dimension, so boatramp automatically adds the matching Vary header (e.g. Vary: accept-language) to the response — a downstream cache then keys on it and never serves one visitor’s locale redirect to another. Conditions that read only the URL + deploy content (path, file_exists) add no Vary.

headers

Each rule sets or removes response headers on matching paths. All matching rules apply, in order.

FieldTypeDescription
matchespatternPath pattern (named matches because for is a keyword).
setmap<string, string>Headers to set.
unsetlist<string>Header names to remove.
headers: [ (matches: "/assets/*", set: { "Cache-Control": "public, max-age=31536000, immutable" }) ],

cache

FieldTypeDescription
defaultstring?Default Cache-Control for responses not covered by a header rule.

proxy_allow

Upstream hosts a proxy rewrite may target. An entry is an exact host or a .suffix for a subtree (.internal.example.com). When the list is empty, proxying to any public host is allowed; private, loopback, and link-local addresses are always blocked as an SSRF guard, regardless of this list. To proxy to a private address, declare a gateway upstream instead.

handlers

A WebAssembly handler bound to a route. Matched after redirects, before static lookup. See Deploy a handler.

FieldTypeDefaultDescription
routepatternRoute pattern.
methodslist<string>[] (all)HTTP methods answered (GET, POST, …).
componentstringPath to the component .wasm within the deployment.
importslist<string>[]Requested capabilities — see imports.
limitsHandlerLimitsOptional resource caps, intersected with the site caps at activation.
envmap<string, string>{}Static environment variables. Never secrets — a credential-shaped value is rejected at validate; use [handlers].secrets in boatramp.cfg for those.
invoke_targetslist<string>[]Function names this handler may call via the invoke import — see invoke_targets.

imports

The capability vocabulary a handler may request. An unrecognized import is rejected at validate.

ImportGrants
invokeCall sibling functions by name, gated by invoke_targets.
wasi:httpOutbound HTTP.
wasi:keyvaluePer-site KV store.
wasi:blobstorePer-site blob store.
wasi:messagingPublish / subscribe on topics.
sqlThe default per-site SQL database (managed libsql), opened as sql.open("").
sql:<name>A specific operator-configured named database (e.g. sql:analytics), opened as sql.open("<name>") — its own connection + role, for least-privilege isolation.
sql:*Every named database the site exposes (a convenience grant; the site’s allow_imports is still the hard ceiling).
wasi:io, wasi:clocks, wasi:random, wasi:loggingStandard host facilities (wasi:logging messages are captured into the site’s logs alongside stdout/stderr).

The site’s allow_imports is the allowlist; a handler requesting an import the site does not permit is denied at activation.

invoke_targets

The deny-by-default allowlist of sibling function names this handler may call through the invoke import. Each entry may use * wildcards (* = any function, img-* = a family, resize = one literal). It is only consulted when imports contains invoke (which the site’s allow_imports must also permit); an empty list means the handler cannot invoke anything even with the import.

handlers: [ (route: "/api", component: "api.wasm", imports: ["invoke"], invoke_targets: ["resize", "img-*"]) ],

limits (HandlerLimits)

FieldTypeDescription
memory_mbu32?Max linear memory, MiB.
timeout_msu32?Wall-clock timeout, ms.
fuelu64?CPU budget in wasmtime fuel units (deterministic instruction-count bound). Omitted = unmetered.

Each field may only lower the corresponding site cap, never raise it. A request handler is connection-bearing, so its timeout_ms is additionally capped by the engine’s sync ceiling (handlers.sync_max_timeout_ms, default 10s) — a route that declares more is clamped back down. Genuinely long-running work belongs on the async path (a durable --async invocation or a workflow), not a request handler held open.

consumers

A component invoked once per message on a topic. See Run consumers, crons, and streams.

FieldTypeDescription
topicstringTopic to subscribe to. A bus:<topic> prefix subscribes to the shared, project-scoped bus (so producers and consumers in different components meet on one topic); a plain topic is site-private.
componentstringPath to the component .wasm.
importslist<string>Requested capabilities.
groupstringConsumer group. Empty (default) = the competing-consumer work-queue (one consumer handles each message); a non-empty name = a durable fan-out subscriber that receives every message on its own cursor, independent of other groups.
startlatest | earliestWhere a non-empty group starts on first subscription: latest (default — only new events) or earliest (replay the retained backlog). Ignored for the work-queue.

crons

A scheduled invocation of a declared handler route.

FieldTypeDefaultDescription
schedulestringStandard 5-field cron (minute hour dom month dow).
routestringHandler route to invoke; must be served by a declared handler.
overlapenumSkipSkip a tick if the previous run is still in flight, or Allow concurrent runs.

streams

A host-level endpoint that fans out messaging topics to connected clients.

FieldTypeDefaultDescription
routestringRoute the endpoint is served at.
topicslist<string>Topics broadcast to clients (server→client).
websocketboolfalseServe as a WebSocket instead of SSE (adds a client→server direction).
publish_topicstring?For a WebSocket, the topic client→server messages publish to. Omitted = receive-only.

Patterns

Route, redirect, rewrite, and header patterns share one matcher syntax:

TokenMatchesCapture
:nameOne path segment:name in to
* / /*The rest of the path:splat in to
literalItself

Path normalization (dot-segment collapsing, the trailing-slash policy) runs before matching, so patterns always see a canonical path and cannot be bypassed with .. or a double slash. See The request pipeline.