Menu

Context (ctx)

The second argument of the handler is ctx. Each member is present only when the release requests the capability in its manifest (permissionsRequested) and an administrator has granted it. Otherwise the member is undefined: check before use.

Member Capability Requested with Granted by
ctx.db Private SQLite database permissionsRequested.database Site admin, Developer, workspace admin, Super Admin
ctx.storage Private file storage permissionsRequested.storage same
ctx.env Site secrets permissionsRequested.secrets same
ctx.mail Outbound mail permissionsRequested.email Super Admin only
fetch (global) Outbound HTTPS permissionsRequested.externalHttp (host list) Super Admin only
ctx.dbs Named workspace databases permissionsRequested.workspaceDatabases coarse switch plus a per-database grant
ctx.admin Admin extension verification (private /_admin/ calls only) not a grant

Grants are managed under Settings, Permissions in the site admin, or with GET/PATCH /admin/api/v1/sites/{id}/capabilities (the PATCH body carries expected_policy_version and a set object; a stale version is rejected). Grant changes apply to the next broker call, with no redeploy. A grant never raises a quota or supplies a secret value. See Manifest for the intersection rule.

Presence of a member does not guarantee a call succeeds: each call is re-authorized, and quotas, resource availability and sender configuration can still refuse it. Calls reject with a coded error (see Limits and errors).

ctx.db: raw SQL

ctx.db.query(sql: string, params?: SqlParam[]): Promise<SiteDbResult>
ctx.db.execute(sql: string, params?: SqlParam[]): Promise<SiteDbResult>
ctx.db.transaction(statements: { sql: string; params?: SqlParam[] }[]): Promise<SiteDbResult[]>
// SiteDbResult = { columns: string[]; rows: unknown[][]; rowsAffected: number; lastInsertRowid: number | bigint | null }

What the broker enforces (these are SQLite hardening rules, independent of your code):

ctx.storage: private files

ctx.storage.get(key: string): Promise<Uint8Array | null>
ctx.storage.put(key: string, value: string | Uint8Array): Promise<void>
ctx.storage.append(key: string, value: string | Uint8Array): Promise<void>
ctx.storage.delete(key: string): Promise<boolean>
ctx.storage.list(prefix: string, options?: { limit?: number }): Promise<string[]>
ctx.storage.sendFile(key: string, init?: { headers?: HeadersInit }): Response
const bytes = await ctx.storage.get("content/about.txt");
if (bytes === null) return new Response("Not found", { status: 404 });
return new Response(bytes, { headers: { "content-type": "text/plain; charset=utf-8" } });

ctx.env: secrets

ctx.env.get(name: string): Promise<string | undefined>

Reads a site secret that an administrator stored (PUT /admin/api/v1/sites/{id}/secrets/{name}). It is not the process environment. An unknown or reserved name resolves to undefined. Secret values are redacted from console output. Never return a secret to the browser and never put one in a manifest.

ctx.mail: outbound mail

ctx.mail.send({ to: string | string[], subject: string, text: string, html?: string }): Promise<{ messageId: string }>

send resolves when the message is accepted into the durable outbox, not when it is delivered. The sender identity is the site's, not chosen by your code; there are no attachments. Policy limits apply (by default at most 10 messages per execution, 10 recipients and 256 KiB per message; the outbox holds at most 100 queued messages and 5 MiB per site). Delivery is asynchronous, retried and expires after 24 hours; operators see its state on the site Activity page.

Status. Messages are delivered through the platform's SMTP server, which a platform administrator sets up under Platform settings → Mail. Until then the mail capability cannot be granted to a site (mail_not_configured). Mail is always From the address configured there (by default the platform's, or the site's own SMTP server if the owner set one under Site settings → Mail); your code cannot choose it. Mail is not part of the conformance suite.

ctx.jobs: background jobs

ctx.jobs.enqueue(name: string, payload?: unknown, options?: { delaySeconds?: number; key?: string }): Promise<{ runId: number }>

Present only when the site declares jobs in its manifest and an administrator granted it jobs. It queues a run of one of your own jobs; see Jobs for the payload, the idempotency key and the error codes.

fetch: outbound HTTPS

fetch(input: string | URL, init?: { method?: string; headers?: HeadersInit; body?: string | Uint8Array }): Promise<FetchResponse>
// FetchResponse: status, ok, headers, text(), json(), arrayBuffer()

Status. In the production composition fetch uses the in-process HTTPS egress broker. The separate egress process (RemoteEgressBroker) is not built and returns Unimplemented; fetch is proven end to end on all three engines against a local TLS peer (allowed host, refused host, plain HTTP and IP literals), but it is not in the cross-engine conformance suite and has not been run against a real public host. Test it on your own host before depending on it.

ctx.dbs: named workspace databases

When a release requests permissionsRequested.workspaceDatabases: ["catalog"], the coarse workspace_databases capability is granted, and a live catalog grant exists for that database, ctx.dbs.get("catalog") returns a handle with query, execute and transaction (same shapes as ctx.db). Names are 1 to 64 letters, digits, underscores or hyphens; a release may request at most 64.

Status. The runtime surface exists, but it is not declared in sdk/microhost.d.ts, the SDK modules (microhost/db) do not wrap it, and project notes record its integration as unverified. Use ctx.db unless you have to share data across sites.

ctx.admin: private extension calls

Present only on private /_admin/ invocations dispatched by the control plane. Use the global verifyAdminRequest(request, ctx) rather than touching it. See Admin extensions.