Route kinds and compute classes
Every file in src/api/ has two independent concerns:
kinddescribes handler behavior (taskversus an HTTP endpoint). The legacyserverlessandhotvalues remain compatible for one migration window.compute.classdescribes execution:functionordedicated.
New HTTP endpoints and tasks default to function with 1gb of memory.
Canonical compute contract
export const route = {
kind: 'task',
compute: {
class: 'function',
memory: '4gb', // 1gb | 4gb | 6gb | 8gb | 12gb
},
timeout: 60_000,
};
| Class | Use it for | Memory contract |
|---|---|---|
function (default) |
Stateless HTTP endpoints, native modules, CPU work, media/doc processing, AI payloads, and ordinary tasks | 1gb default; 4gb, 6gb, 8gb, or 12gb selectable |
dedicated |
WebSockets, listening sockets, process-local state, background loops, and always-on work | Machine profile; optional memory/CPU are preserved in the manifest |
On managed Vura, server and hybrid pages currently use the persistent Dedicated project runtime because their request-time SSR handler needs the full server build context. They are not placed in an individual Function endpoint.
Dedicated manifests also carry the legacy machine.memoryMb / machine.cpus
shape so current platform hot-task sizing honors canonical memory and CPU
requests during the compatibility window.
Use vura routes inspect --json or vura runtime advise --json to see the
effective class, memory, CPU, timeout, provider recommendation, confidence, and
reasons before deployment.
Deployment-relevant route config is a static literal contract. Strings, finite numbers,
numeric separators, booleans, null, arrays, nested objects, and as const are
supported. Calls, referenced identifiers, spreads, computed properties, and
template literals are rejected with a line/column error; scanning never imports
or evaluates application code. Deployment-affecting page fields such as mode,
revalidate, and tags must use static literals, and modes must be quoted
strings. Explicit presentation references such as styles: [baseStyles] are
omitted from the manifest and evaluated later by the renderer.
Annotation
// shorthand — kind only
export const kind = 'hot';
// route object — kind + config fields
export const route = {
kind: 'task',
compute: { class: 'function', memory: '1gb' },
retries: 2,
timeout: 60_000,
};
When both kind and route are exported from the same file, route wins. Use the shorthand when you have no additional config.
serverless (default)
Stateless, per-request. Runs inside the unified Node server (persistent hosts) or as an individual Lambda/Worker function (serverless adapters). No state persists between requests. No timeout beyond the adapter limit.
Exports recognized:
| Export | Description |
|---|---|
GET(req, reply) |
Handle HTTP GET |
POST(req, reply) |
Handle HTTP POST |
PUT(req, reply) |
Handle HTTP PUT |
DELETE(req, reply) |
Handle HTTP DELETE |
PATCH(req, reply) |
Handle HTTP PATCH |
HEAD(req, reply) |
Handle HTTP HEAD |
OPTIONS(req, reply) |
Handle HTTP OPTIONS |
Any subset of these. req is the Celsian request object; reply has .json(), .text(), .html(), .status(), .redirect().
Lifecycle: request comes in → handler runs → response sent → handler scope is discarded.
hot
Persistent process. The server holds a WebSocket open for each connection. In-memory state (sets, maps, objects) lives for the lifetime of the server process, not the request. There is no timeout.
Exports recognized:
| Export | Description |
|---|---|
websocket(peer, req) |
Called once per connection on open. |
GET / POST / ... |
Plain HTTP methods work alongside WebSocket on the same route file. |
websocket(peer, req) contract:
peer — HotPeer:
peer.id— unique connection id (string)peer.send(data)— send string or ArrayBuffer; no-op after closepeer.close(code?, reason?)— close this connectionpeer.on('message' | 'close', cb)— subscribe to eventspeer.broadcast(data, excludeSelf = true)— send to all peers on the same concrete URL path. Broadcast is path-keyed (e.g./api/rooms/7), not pattern-keyed (/api/rooms/:id), so it delivers only to peers in the same "room".
req — HotRequest:
req.url— full URL string of the upgrade requestreq.headers—Headersobject from the upgrade requestreq.query—URLSearchParamsfrom the upgrade URLreq.params— path params extracted from the route pattern (e.g.{ room: '7' }for/api/rooms/:room)
route config fields:
| Field | Type | Default | Effect |
|---|---|---|---|
origins |
string[] |
unset | Origin allowlist for the WebSocket handshake. When set, upgrade requests whose Origin header is not on the list are rejected with 403 before the handshake. Entries are compared as URL origins, case-insensitively (trailing slashes/paths/default ports are normalized away). Requests with no Origin header always pass — browsers always send one, and non-browser clients can forge any value anyway. Default is open: without origins, any site can open a WebSocket to this route — set it for any cookie-authenticated app. Entries must be inline string literals; unsafe dynamic forms fail the build instead of silently losing the allowlist. origins: [] is preserved and denies all browser origins. |
export const route = { kind: 'hot', origins: ['https://app.example.com'] };
Known limitation — backpressure: peer.send() is fire-and-forget with no bufferedAmount cap. A slow consumer can buffer unbounded data in the socket write queue. Implement your own flow-control in the message handler for high-throughput binary streams.
Lifecycle: server boots → per-connection websocket(peer, req) called on upgrade → peer.on('message', ...) fires per frame → peer.on('close', ...) fires on disconnect. State in module scope persists across all connections for the lifetime of the process.
task
Off the request path. Not exposed as a regular HTTP endpoint — calls go through /__tasks. The handler is the POST export and receives a context object.
Exports recognized:
| Export | Description |
|---|---|
POST(ctx) |
Handler. ctx.attempt (1-based retry count), ctx.input (parsed JSON body or { _cron: true } for scheduled runs). |
route |
Object with kind: 'task' plus optional retries (default 0) and timeout in ms (default 30 000). |
schedule |
Top-level cron expression sugar (standard five-field). Registers a cron trigger automatically on server start. Can be exported separately from route; does not need to be inside route. |
route config fields:
| Field | Type | Default | Effect |
|---|---|---|---|
retries |
number |
0 |
Number of retry attempts after a failure. |
timeout |
number (ms) |
30000 |
Milliseconds before the run is considered timed-out and retried (if retries remain). |
/__tasks admin interface:
| Request | Effect |
|---|---|
GET /__tasks |
List all registered task routes. |
POST /__tasks/<name> |
Trigger task <name> immediately with an optional JSON body as ctx.input. |
vura tasks CLI:
vura tasks list # list task routes and their schedules
vura tasks run <name> # trigger immediately
vura tasks run <name> --input '{"k":1}' # with input
Lifecycle: POST to /__tasks/<name> → handler runs → retried up to retries times on throw → result returned as JSON. Scheduled tasks fire via the cron engine inside the running server.
Per-adapter support matrix
| Route kind | Node / VPS | Docker | Fly.io | Railway | CF Workers | Lambda |
|---|---|---|---|---|---|---|
| Serverless | yes | yes | yes | yes | yes | yes |
| Hot (WebSocket) | yes | yes | yes | yes | no | no |
| Task / cron | yes | yes | yes | yes | via scheduled event |
yes (EventBridge) |
Hot routes require a persistent process that can hold a WebSocket open. Cloudflare Workers and Lambda terminate the process between invocations and cannot fulfill this contract. vura build emits a warning at build time when a hot route is detected and the selected adapter cannot support it:
[vura] N hot route(s) cannot run on <adapter> and were not bundled: /api/live/room — deploy them to a persistent host (see /self-host/)
The hot routes are not silently omitted — they are excluded with an explicit named warning.
See the Deploy rung and Adapters reference for the full per-adapter picture.