Arcwayv0.3.0

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:

PropertyTypeDescription
ctx.dbKnex instanceDatabase query builder with read-write access
ctx.logLoggerStructured logger (debug, info, warn, error)
ctx.eventsEventBusEvent emitter (emit, subscribe)
ctx.cacheCacheKey-value cache (get, set, delete, wrap)
ctx.queueQueuePersistent queue (push, pop, remove)
ctx.filesFileStorageFile storage (read, write, delete, list, exists)
ctx.mailMailEmail service (send, queue)
ctx.vaultVaultCrypto toolkit (password hashing, encryption, JWT, IDs, key derivation)
ctx.errfunctionBuild an error response: ctx.err(message, status)
ctx.pluginsPluginManagerLoaded plugins and the capability registry
ctx.callbacksCallbackRegistryRegister sealed inbound-callback tokens (OAuth redirects, webhooks)
ctx.metaobjectApp 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.

MethodDescription
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.

MethodDescription
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);

See Plugins & Capabilities.

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).

MethodDescription
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.

PropertyTypeDescription
meta.builtAtstring | nullISO 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

PropertyTypeDescription
req.idstringUnique ID for this request (for tracing)
req.ipstringClient IP address (respects trustProxy config)
req.methodstringHTTP method ('GET', 'POST', 'PUT', etc.)
req.pathstringURL pathname (e.g., '/api/users/123')
req.queryobjectQuery parameters merged with route params
req.bodyanyParsed JSON body (or raw string for non-JSON)
req.rawBodystringRaw request body string (POST/PUT/PATCH/DELETE only)
req.headersobjectRequest headers (lowercase keys)
req.cookiesobjectParsed cookies
req.sessionobjectSession 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

PropertyTypeDescription
event.namestringEvent name (e.g., 'users/created')
event.payloadanyData 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

PropertyTypeDescription
ctx.payloadanyData 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

PropertyTypeDescription
page.pathnamestringURL path (e.g., '/blog/hello-world')
page.queryobjectRoute parameters (e.g., { slug: 'hello-world' })
page.headersobjectRequest headers (lowercase keys)
page.cookiesobjectParsed cookies
page.sessionobjectSession data (if configured)

Return Value

Page middleware can redirect, block, or pass through:

ReturnEffect
{ 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

PropertyRoute HandlerListenerJobPage Middleware
ctx.dbyesyesyesyes
ctx.logyes (+ requestId)yesyesyes
ctx.eventsyesyesyesyes
ctx.cacheyesyesyesyes
ctx.queueyesyesyesyes
ctx.filesyesyesyesyes
ctx.mailyesyesyesyes
ctx.metayesyesyesyes
ctx.reqyes
ctx.eventyes
ctx.payloadyes
ctx.pageyes

On this page