Context Reference
Complete reference for the ctx object in route handlers, listeners, jobs, and page middleware
Every handler in Arcway receives a ctx object containing infrastructure services and request-specific data. The shape of ctx varies depending on the context: route handlers, event listeners, jobs, or page middleware.
Shared Infrastructure
All contexts include these infrastructure services:
| Property | Type | Description |
|---|---|---|
ctx.db | Knex instance | Database query builder with read-write access |
ctx.log | Logger | Structured logger (debug, info, warn, error) |
ctx.events | EventBus | Event emitter (emit, subscribe) |
ctx.cache | Cache | Key-value cache (get, set, delete, wrap) |
ctx.queue | Queue | Persistent queue (push, pop, remove) |
ctx.files | FileStorage | File storage (read, write, delete, list, exists) |
ctx.mail | Email service (send, queue) | |
ctx.vault | Vault | Crypto toolkit (password hashing, encryption, JWT, IDs, key derivation) |
ctx.err | function | Build an error response: ctx.err(message, status) |
ctx.plugins | PluginManager | Loaded plugins and the capability registry |
ctx.callbacks | CallbackRegistry | Register sealed inbound-callback tokens (OAuth redirects, webhooks) |
ctx.meta | object | App build metadata (builtAt, buildNumber, branch, sha, version) |
See Infrastructure for detailed documentation on each service.
ctx.vault
The crypto toolkit on every context. Alongside password hashing, encryption, and ID generation, it exposes keyless JWT helpers and deterministic key derivation rooted in vault.masterSecret.
| Method | Description |
|---|---|
ctx.vault.jwtEncode(payload, options?) | Sign a JWT with Arcway's managed jwt key — no secret argument. options: expiresIn, issuer, audience, algorithm. |
ctx.vault.jwtDecode(token, options?) | Verify and decode a JWT signed by the managed key — no secret argument. options: issuer, audience. |
ctx.vault.deriveKey(label) | App-specific symmetric key (32 bytes, base64url) deterministically derived from the master secret. Same master + label always yields the same key. |
ctx.vault.deriveKeyPair(label, { curve }) | Deterministic EC keypair { publicKey, privateKey } derived from the master secret. curve defaults to prime256v1 (P-256). |
const apiKey = ctx.vault.deriveKey('my-service-api');
const { publicKey, privateKey } = ctx.vault.deriveKeyPair('vapid');See Vault for the master secret, keyring rotation, and the full crypto API.
ctx.plugins
The plugin registry. Call a plugin's exported handlers directly from any route, job, or listener — plugins are code-only and own no HTTP routes.
| Method | Description |
|---|---|
ctx.plugins.get(id) | Returns the plugin's exported functions (from its exports.js) for direct invocation. |
const settings = await ctx.plugins.get('web-search').loadSettings(workspaceId);ctx.callbacks
Register a sealed, framework-owned inbound callback so an external service (OAuth redirect, webhook) can reach your code without any route or plugin defining a public endpoint. The inbound hit lands at /_system/callback; dispatch routes to a handler in the top-level callbacks/<name>.js directory (or a plugin's own callback handler).
| Method | Description |
|---|---|
ctx.callbacks.register({ handler, payload, oneTime, ttl }) | Writes a callback registration and returns an opaque sealed token. handler: target name (callbacks/<name>.js or plugins/<id>/<name>). payload: arbitrary JSON handed to the handler as ctx.callback.payload. oneTime: consume on first use. ttl: lifetime in seconds. |
const token = await ctx.callbacks.register({
handler: 'google',
payload: { userId },
oneTime: true,
ttl: 600,
});See Callbacks for token semantics, security properties, and handler shape.
ctx.meta
Arcway exposes app metadata everywhere through ctx.meta. It is collected once at process boot and cached in memory. Git-derived fields are null when git metadata is unavailable in the running environment.
| Property | Type | Description |
|---|---|---|
meta.builtAt | string | null | ISO timestamp for when the process metadata was collected |
| meta.version | string | null | Generic build version for cache-busting |
| meta.buildNumber | number | null | Monotonic git rev-list --count HEAD value |
| meta.branch | string | null | Git branch name at boot time |
| meta.tag | string | null | Nearest git tag, if available |
| meta.dirty | boolean | null | Whether the worktree had uncommitted changes |
| meta.environment | string | Build/runtime environment name |
| meta.nodeVersion | string | Node version used by the running process |
| meta.shortSha | string | null | Short git SHA (7 chars) |
| meta.sha | string | null | Full git SHA |
| meta.packageVersion | string | null | Root package.json version |
Route Handler Context
API route handlers (api/ directory) receive ctx with a req property containing all request data:
export const POST = {
handler: async (ctx) => {
const { db } = ctx;
// ctx.req — request data
// db, log, events, etc. — infrastructure (destructured from ctx)
const user = await db('users').where('id', ctx.req.query.id).first();
return { data: user };
},
};ctx.req
| Property | Type | Description |
|---|---|---|
req.id | string | Unique ID for this request (for tracing) |
req.ip | string | Client IP address (respects trustProxy config) |
req.method | string | HTTP method ('GET', 'POST', 'PUT', etc.) |
req.path | string | URL pathname (e.g., '/api/users/123') |
req.query | object | Query parameters merged with route params |
req.body | any | Parsed JSON body (or raw string for non-JSON) |
req.rawBody | string | Raw request body string (POST/PUT/PATCH/DELETE only) |
req.headers | object | Request headers (lowercase keys) |
req.cookies | object | Parsed cookies |
req.session | object | Session data (empty {} if no session or invalid) |
Route params are merged into req.query:
// api/users/[id].js — request to /api/users/123?include=posts
export const GET = {
handler: async (ctx) => {
ctx.req.query.id; // '123' (from route param)
ctx.req.query.include; // 'posts' (from query string)
},
};Logger is request-scoped:
In route handlers, ctx.log automatically includes the requestId in every log entry, making it easy to trace all logs for a single request.
Return Value
Route handlers return an object with optional status, data, error, and session:
// Success (status defaults to 200)
return { data: { id: 1, name: 'Alice' } };
// Created
return { status: 201, data: { id: 1 } };
// Error
return { status: 404, error: { code: 'NOT_FOUND', message: 'User not found' } };
// Set session
return { data: { ok: true }, session: { userId: 1 } };
// Clear session
return { data: { ok: true }, session: null };
// No session change (default)
return { data: { ok: true } };Event Listener Context
Event listeners (listeners/ directory) receive ctx with an event property:
// listeners/users/created.js
export default async (ctx) => {
const { mail } = ctx;
// ctx.event — event data
// mail, db, log, etc. — infrastructure (destructured from ctx)
const { id, email } = ctx.event.payload;
await mail.send({
to: email,
subject: 'Welcome!',
html: '<h1>Welcome!</h1>',
});
};ctx.event
| Property | Type | Description |
|---|---|---|
event.name | string | Event name (e.g., 'users/created') |
event.payload | any | Data passed to events.emit() |
No return value — listeners are fire-and-forget. Errors are caught and logged but don't propagate to the emitter.
Job Handler Context
Job handlers (jobs/ directory) receive ctx with the job payload as a top-level property:
// jobs/send-report.js
export default {
name: 'send-report',
handler: async (ctx) => {
const { db, mail } = ctx;
// ctx.payload — job data
// db, mail, log, etc. — infrastructure (destructured from ctx)
const { userId, reportType } = ctx.payload;
const data = await generateReport(db, userId, reportType);
await mail.send({
to: data.email,
subject: `Your ${reportType} report`,
html: renderReport(data),
});
},
retries: 3,
};ctx.payload
| Property | Type | Description |
|---|---|---|
ctx.payload | any | Data passed when the job was enqueued |
For scheduled (cron) and continuous jobs, payload is undefined since they're triggered by schedule rather than enqueued with data.
Page Middleware Context
Page middleware (pages/_middleware.js) receives ctx with a page property containing request data specific to page rendering:
// pages/_middleware.js
export default async (ctx) => {
// ctx.page — page request data
// db, log, etc. — infrastructure (available on ctx)
if (!ctx.page.session?.userId) {
return { redirect: '/login' };
}
};ctx.page
| Property | Type | Description |
|---|---|---|
page.pathname | string | URL path (e.g., '/blog/hello-world') |
page.query | object | Route parameters (e.g., { slug: 'hello-world' }) |
page.headers | object | Request headers (lowercase keys) |
page.cookies | object | Parsed cookies |
page.session | object | Session data (if configured) |
Return Value
Page middleware can redirect, block, or pass through:
| Return | Effect |
|---|---|
{ redirect: '/login' } | 302 redirect |
{ redirect: '/login', status: 301 } | Redirect with custom status |
{ status: 403 } | Block with status code |
{ status: 403, body: '<h1>Forbidden</h1>' } | Block with custom HTML |
undefined (return nothing) | Continue to render the page |
Context Comparison
| Property | Route Handler | Listener | Job | Page Middleware |
|---|---|---|---|---|
ctx.db | yes | yes | yes | yes |
ctx.log | yes (+ requestId) | yes | yes | yes |
ctx.events | yes | yes | yes | yes |
ctx.cache | yes | yes | yes | yes |
ctx.queue | yes | yes | yes | yes |
ctx.files | yes | yes | yes | yes |
ctx.mail | yes | yes | yes | yes |
ctx.meta | yes | yes | yes | yes |
ctx.req | yes | — | — | — |
ctx.event | — | yes | — | — |
ctx.payload | — | — | yes | — |
ctx.page | — | — | — | yes |