Site Mills

Site Mills API Reference

This page documents the two primary code surfaces used in generated Site Mills projects: the backend Sandbox Runtime SDK (inside server handlers/jobs) and the browser-facing frontend API wrappers (auth, backend calls, realtime, and room clients).

Backend: server/handlers/*.ts and server/jobs.ts Frontend: public/api/*.js and window globals All examples are async and Promise-based

Overview

Handler code runs inside the Site Mills Sandbox Runtime. Browser code calls those handlers through generated API wrappers or the backend bridge. Realtime and room APIs are separate websocket-based channels for low-latency interactions.

Layer Primary Entry Point Typical Use
Backend handler export async function routeName(ctx, params) CRUD, business rules, integrations, server validation
Backend job export const jobs = { jobName: async (ctx, params) => {} } Scheduled/manual background work, rollups, automation
Frontend auth window.auth Login/logout/current-user checks
Frontend realtime public/api/realtime-client.js Pub/sub style channel messaging
Frontend rooms public/api/room-client.js Authoritative room state, game loops, multiplayer
Frontend p2p public/api/p2p-client.js WebRTC session setup (TURN/ICE + signaling helpers)

Sandbox Runtime SDK (Handler Context)

The handler context is available as the first argument in backend routes: export async function myRoute(ctx, params).

Database

ctx.db.collection(name)
  • find, findOne, findWithOptions, count, distinct
  • insertOne, insertMany
  • updateOne, updateMany, findOneAndUpdate, replaceOne
  • deleteOne, deleteMany, findOneAndDelete
  • aggregate for document-style pipelines

Metrics

await ctx.metrics.track(eventName, properties?)
  • Use snake_case event names
  • Properties are persisted as strings
  • Use started/completed/failed pairs for long flows

Logs

console.* or ctx.logs.info|warn|error(message, data)
  • Request-scoped runtime logs
  • No credit cost for log writes
  • Designed for debugging and runtime observability

User Context

ctx.user
  • Present for authenticated requests
  • Null/undefined for public requests
  • Common fields: id, email, isAuthenticated
Use handler code for request/response flows. Return JSON-serializable objects only.
export async function listOrders(ctx, params) {
  const limit = Math.min(Number(params?.limit ?? 20), 100);

  const orders = await ctx.db
    .collection('orders')
    .find({})
    .sort({ createdAt: -1 })
    .limit(limit);

  await ctx.metrics.track('orders_listed', {
    limit: String(limit),
    userId: ctx.user?.id ?? 'anonymous'
  });

  return { orders };
}

Complete Sandbox Runtime API Surface

This matrix is based on the runtime bootstrap + integration manifest currently wired in the Site Mills Sandbox Runtime. It covers core ctx APIs plus all registered integration namespaces.

Integration namespace rows are generated during build from integration manifests and provider sources to prevent docs drift.

Namespace / API Methods Notes
ctx.user, ctx.userId, ctx.email Identity fields populated from runtime auth context Null/empty for anonymous requests
ctx.db get, set, find, findOne, insert, update, delete, aliases, and collection(name) Includes chainable query builder + aggregate + distinct
ctx.metrics track(eventName, properties?) Properties are serialized as strings
ctx.logs + console.* log, info, warn, error Routed through runtime logs integration
ctx.session get, set, delete Handler-session scope; not available for headless jobs
ctx.http + ctx.sendResponse status(code).json|send, json, sendResponse, response Express-style response helper wrappers
ctx.realtime No exported polyfill methods detected Realtime runtime namespace.
ctx.storage No exported polyfill methods detected Object Storage runtime namespace.
ctx.orders No exported polyfill methods detected Orders and Payments runtime namespace.
ctx.env No exported polyfill methods detected Environment Secrets runtime namespace.
ctx.permissions No exported polyfill methods detected Permissions runtime namespace.
ctx.email No exported polyfill methods detected Email runtime namespace.
ctx.http No exported polyfill methods detected External HTTP runtime namespace.
ctx.ai No exported polyfill methods detected AI Workflows runtime namespace.
ctx.queue No exported polyfill methods detected Message Queue runtime namespace.
ctx.p2p No exported polyfill methods detected Peer-to-Peer runtime namespace.
sdk.onRealtime sdk.onRealtime(channel, handler) Registers backend handler for inbound realtime channel events
Room + sdk.registerRoom sdk.registerRoom(roomType, RoomClass), room lifecycle hooks Authoritative room runtime for multiplayer/state loops
External HTTP through global fetch() is intentionally blocked. Use ctx.httpClient for outbound requests.

AI Workflows (ctx.ai)

AI workflows are asynchronous. You enqueue work from your handler and receive results in a callback route.

Method Required Fields Optional Fields
ctx.ai.enqueue(opts) prompt, workflowType (CHAT or ONE_SHOT), callbackRoute model, fileIds, enableGoogleSearch, callbackContext, maxRetries, timeoutMs
ctx.ai.chat(opts) prompt, callbackRoute Same optional fields; workflow type forced to CHAT
ctx.ai.oneShot(opts) prompt, callbackRoute Same optional fields; workflow type forced to ONE_SHOT
ctx.ai.analyze(opts) Alias of oneShot Alias of oneShot
Supported models: gemini-3-pro, gemini-3-flash, gemini-3-flash-lite (default).
export async function summarizeTicket(ctx, params) {
  const job = await ctx.ai.oneShot({
    prompt: `Summarize this ticket in 5 bullets: ${params.ticketText}`,
    callbackRoute: 'handleTicketSummary',
    model: 'gemini-3-flash-lite',
    callbackContext: { ticketId: params.ticketId },
    fileIds: params.attachmentFileIds || []
  });

  return { queued: true, workflowId: job.workflowId };
}

Queue Workflows (ctx.queue)

Queue workflows let handlers offload async work to the runtime message queue. Messages are retried with backoff, can be explicitly acked/no-acked by the handler result, and move to dead-letter state when retries are exhausted or a handler requests terminal no-ack.

Method Required Fields Optional Fields
ctx.queue.enqueue(opts) queueName, handlerRoute, payload metadata, maxRetries (0..10), retryDelayMs (1000..900000)
ctx.queue.send(opts) Alias of enqueue Alias of enqueue
ctx.queue.ack(result?) None Optional result payload persisted as handler result context
ctx.queue.nack(opts?) None requeue, retryDelayMs, reason, result
Queue handlers receive payload shape: { messageId, projectId, branchId, environment, dataScope, queueName, attempt, maxRetries, payload, metadata, enqueuedAt }.
export async function queueOrderReceipt(ctx, params) {
  const queued = await ctx.queue.enqueue({
    queueName: 'orders.receipts',
    handlerRoute: 'handlers/queue_handlers.processOrderReceipt',
    payload: {
      orderId: params.orderId,
      email: params.email
    },
    maxRetries: 3,
    retryDelayMs: 5000
  });

  return { queued: true, messageId: queued.messageId };
}

export async function processOrderReceipt(ctx, message) {
  try {
    await ctx.email.send({
      to: message.payload.email,
      subject: 'Order receipt',
      body: `Order ${message.payload.orderId} processed.`
    });

    return ctx.queue.ack({ delivered: true });
  } catch (error) {
    return ctx.queue.nack({
      requeue: true,
      retryDelayMs: 30000,
      reason: 'email_provider_error'
    });
  }
}

Storage and Uploads (ctx.storage)

Storage supports both direct base64 uploads from handlers and staged upload claims from frontend multipart uploads.

Method Purpose
upload({ data, key?, originalFilename?, contentType?, metadata? }) Direct base64 upload to runtime media storage.
get(idOrKey), url(idOrKey), getMetadata(idOrKey) Fetch full object, URL, or metadata.
list(prefix?, limit?), delete(idOrKey) Enumerate and remove stored media.
claimUpload(fileId, { ttlSeconds? }) Claim staged frontend upload and persist it as managed media.
getUploadMetadata(fileId) Inspect staged upload before claim.
// 1) Frontend uploads multipart file to POST /api/v1/upload and gets fileId
// 2) Handler claims it into managed storage
export async function attachAvatar(ctx, params) {
  const claimed = await ctx.storage.claimUpload(params.fileId, { ttlSeconds: 86400 });

  await ctx.db.collection('profiles').updateOne(
    { _id: params.userId },
    { $set: { avatarUrl: claimed.url, avatarFileId: claimed.id } },
    { upsert: true }
  );

  return { avatarUrl: claimed.url, fileId: claimed.id };
}

Other Integration Namespaces

Additional runtime namespaces available in the Site Mills Sandbox Runtime.

ctx.env

list, names, listDetailed, get, require, getOptional
  • Environment-scoped secret and variable lookup
  • Variable names are normalized to uppercase

ctx.permissions

permissions, roles, assignments, check/can/require, bootstrap
  • Strictly project-scoped resource/action patterns
  • Mutation methods require authenticated actor context

ctx.email

send({ to, subject, body|html|text }) or send(to, subject, body)
  • Supports single or multiple recipients
  • Body can be plain text or HTML content

ctx.httpClient

request, get, post, put, patch, delete
  • Absolute http(s) URLs only
  • SSRF guardrails block localhost/private/internal targets

ctx.orders

enablePayments, getStatus, checkout, getSession, getOrderHistory
  • Checkout URLs must stay on trusted request origin
  • Scope/environment are enforced from runtime context

ctx.queue

enqueue, send, ack, nack
  • Async message dispatch with bounded retries
  • Supports explicit handler ack/no-ack dispositions
  • Terminal failures move to dead-letter status and queue

ctx.realtime + sdk.onRealtime

broadcast, send, sendToUser, onRealtime(channel, handler)
  • Send server events to clients
  • Handle inbound channel events in backend runtime handlers

Room Runtime SDK

class Room, sdk.registerRoom(roomType, RoomClass)
  • Lifecycle hooks: onCreate, onJoin, onMessage, onTick, onLeave, onDispose
  • Messaging helpers: broadcast(type, data), sendTo(playerId, type, data)

Logs Path

ctx.logs.*, console.*
  • Both are normalized and routed to runtime logging pipeline
  • Structured args are captured with truncation guards

Sandbox Runtime SDK (Jobs Context)

Scheduled/manual jobs are defined in server/jobs.ts and use a job-specific context. Jobs are headless, so user/session fields are not available.

Job Metadata

ctx.jobName, ctx.runId, ctx.trigger, ctx.previousRun
  • Use runId for idempotency keys
  • Use previousRun.cursor for incremental processing

Job State

ctx.state.get/set/setCursor
  • Persist cursor/progress between runs
  • Avoid module-level state for durable data

Also Available

ctx.db, ctx.metrics, ctx.realtime, ctx.logs
  • Same database and metrics capabilities as handlers
  • Can emit realtime notifications for completed work

Not Available In Jobs

ctx.user, ctx.session
  • Jobs run without browser request context
  • Pass IDs in params when user-specific processing is needed
export const jobs = {
  rollupDailyOrders: async (ctx, params) => {
    const since = params?.since || ctx.previousRun?.cursor;

    const rows = await ctx.db.collection('orders').find(
      since ? { createdAt: { $gte: since } } : {}
    );

    await ctx.db.collection('order_rollups').insertOne({
      runId: ctx.runId,
      count: rows.length,
      generatedAt: new Date().toISOString()
    });

    await ctx.state.setCursor(new Date().toISOString());
    await ctx.metrics.track('orders_rollup_completed', { count: String(rows.length) });
  }
};

Frontend API (Browser Code)

Frontend code typically lives under public/ and uses wrappers from public/api and generated stubs under public/stubs.

Keep secrets out of browser code. Call backend handlers for privileged operations.

window.auth

Provided by the auth client bootstrap, this API handles login state and OAuth redirects.

Method Signature Behavior
login window.auth.login(returnUrl?) Starts OAuth flow and redirects back to returnUrl.
logout window.auth.logout(returnUrl?) Clears session cookie via same-origin logout endpoint.
getCurrentUser await window.auth.getCurrentUser() Returns user object or null.
isLoggedIn await window.auth.isLoggedIn() Convenience boolean for authenticated state.
clearCache window.auth.clearCache() Clears in-memory user cache.
const user = await window.auth.getCurrentUser();
if (!user?.isAuthenticated) {
  window.auth.login('/dashboard');
}

window.backend.call

Backend handlers are invoked by route name from frontend code. This bridge is used by generated stubs and direct frontend calls.

await window.backend.call(route, payload)

Route names map to exported handler names. Payload is JSON-serializable and passed as the handler's params argument.

const profile = await window.backend.call('getCurrentUser', {});
const leaderboard = await window.backend.call('getLeaderboard', { limit: 25 });

Frontend Upload Endpoint

For browser file uploads, use the runtime multipart endpoint and pass the returned fileId to a backend handler that calls ctx.storage.claimUpload(...).

POST /api/v1/upload (multipart/form-data, field name: file)
// Browser example
const formData = new FormData();
formData.append('file', selectedFile);

const uploadRes = await fetch('/api/v1/upload', {
  method: 'POST',
  credentials: 'include',
  body: formData
});

const uploaded = await uploadRes.json();
// uploaded.fileId -> pass to backend handler for claimUpload
await window.backend.call('attachAvatar', { fileId: uploaded.fileId, userId });
Uploaded files are staged first. They become durable managed media only after your backend handler claims them.

UserService Wrapper

The seeded frontend includes public/api/user-service.ts, which wraps auth and user retrieval for app code.

Method Purpose
getCurrentUser() Returns authenticated user or null.
isLoggedIn() Returns true when current user is authenticated.
onUserChange(callback) Subscribes to auth changes and returns an unsubscribe function.
import { UserService } from './api/user-service.js';

const unsubscribe = UserService.onUserChange((user) => {
  console.log('Auth state changed:', user);
});

// Later:
unsubscribe();

Realtime Client

Use public/api/realtime-client.js for channel/event messaging over websocket.

Function Signature
connect await connect()
subscribe subscribe(channel)
unsubscribe unsubscribe(channel)
on on(channel, event, callback)
off off(channel, event, callback?)
send send(channel, event, data?)
disconnect disconnect()
import { connect, subscribe, on, send } from './api/realtime-client.js';

await connect();
subscribe('chat');

on('chat', 'newMessage', (payload) => {
  console.log('Incoming message:', payload);
});

send('chat', 'sendMessage', { text: 'hello' });

P2P Client

Use public/api/p2p-client.js for browser-managed WebRTC sessions (voice/video/data channels).

Session Setup

const session = await createP2PSession(options?)
  • Creates or joins a signaling session
  • Uses __p2p__:<sessionId> channels under the hood

ICE/TURN

await session.requestIceServers(options?)
  • Fetches runtime-managed TURN/ICE credentials
  • Use response directly in RTCPeerConnection

Signaling

session.sendSignal(eventName, payload, targetUserId?)
  • Exchange offer/answer/ICE payloads with peers
  • Omit target to broadcast, include target for directed signaling
import {
  createP2PSession,
  createPeerConnection,
  attachLocalMedia
} from './api/p2p-client.js';

const session = await createP2PSession({ sessionId: 'call_alpha' });
const ice = await session.requestIceServers();

const pc = createPeerConnection(ice, {
  onIceCandidate: (candidate) => session.sendSignal('ice-candidate', candidate)
});

await attachLocalMedia(pc, { audio: true, video: true });

Room Client

Use public/api/room-client.js for authoritative room state (multiplayer/game loops).

Join and Resolve

const room = await joinRoom(roomType, options?)
  • options.roomId supports explicit or auto assignment
  • options.data sends initial join payload

Room Object

room.state, room.playerId, room.connected
  • onStateChange(callback)
  • onMessage(type, callback)
  • onConnectionChange(callback)
  • onDispose(callback)
  • send(type, payload), leave()
import { joinRoom } from './api/room-client.js';

const room = await joinRoom('arena', { roomId: 'auto' });

room.onStateChange((state) => {
  renderGame(state);
});

room.onMessage('gameOver', (data) => {
  console.log('Game over:', data);
});

room.send('input', { keys: ['ArrowUp', 'Space'] });

Quick Full-Stack Example

Typical flow: frontend calls a handler, handler uses the sandbox runtime SDK, frontend renders result.

// server/handlers/profile.ts
export async function getProfile(ctx, params) {
  if (!ctx.user?.isAuthenticated) {
    return { profile: null };
  }

  const profile = await ctx.db.collection('profiles').findOne({ email: ctx.user.email });
  return { profile: profile || { email: ctx.user.email } };
}

// public/views/profile-view.tsx
const result = await window.backend.call('getProfile', {});
setProfile(result?.profile ?? null);