Database
- find, findOne, findWithOptions, count, distinct
- insertOne, insertMany
- updateOne, updateMany, findOneAndUpdate, replaceOne
- deleteOne, deleteMany, findOneAndDelete
- aggregate for document-style pipelines
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).
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) |
The handler context is available as the first argument in backend routes:
export async function myRoute(ctx, params).
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 };
}
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 |
fetch() is intentionally blocked. Use ctx.httpClient for outbound requests.
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 |
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 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 |
{ 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 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 };
}
Additional runtime namespaces available in the Site Mills Sandbox Runtime.
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.
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 code typically lives under public/ and uses wrappers from
public/api and generated stubs under public/stubs.
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');
}
Backend handlers are invoked by route name from frontend code. This bridge is used by generated stubs and direct frontend calls.
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 });
For browser file uploads, use the runtime multipart endpoint and pass the returned fileId
to a backend handler that calls ctx.storage.claimUpload(...).
// 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 });
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();
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' });
Use public/api/p2p-client.js for browser-managed WebRTC sessions
(voice/video/data channels).
__p2p__:<sessionId> channels under the hoodRTCPeerConnectionimport {
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 });
Use public/api/room-client.js for authoritative room state (multiplayer/game loops).
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'] });
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);