Skip to content
DocsconceptsDocumentation

Function context

The ctx object passed to every query, mutation, and action — and what each kind is allowed to do with it.

Last updated:

Every Lunora function receives a ctx as the first argument of its handler. ctx is the only way a function reaches anything outside itself: the database, auth, storage, the scheduler, and any add-on surfaces. The function kind decides which ctx you get, and the kind constrains what ctx can do. A query cannot write; an action cannot transact against the database.

import { mutation, query } from "@/lunora/_generated/server";

export const list = query.query(async ({ ctx }) => {
    return ctx.db.query("messages").take(50); // read-only
});

export const send = mutation.mutation(async ({ ctx, args }) => {
    if (!ctx.auth.userId) throw new Error("must be signed in");
    await ctx.db.insert("messages", args); // transactional write
});

The three context types

ContextFunction kindctx.dbSide effects
QueryCtxqueryDatabaseReadernone (pure reads)
MutationCtxmutationDatabaseWritertransactional writes against one shard
ActionCtxactionDatabaseWriterunrestricted: fetch, external APIs, composition

QueryCtx

Read-only. ctx.db is a DatabaseReader (get, query, normalizeId, plus the ctx.db.system reader for _scheduled_functions / _storage). Storage and vectors are present but read-only (ReadOnlyStorage, VectorSearchReader). A query may compose other queries with ctx.runQuery, observing the same transaction snapshot. There is no runMutation/runAction, because a query is not allowed to write — and the dispatch enforces it at runtime, so casting past the type gets a RUN_KIND_FORBIDDEN error rather than a write smuggled into a subscription re-run.

MutationCtx

Reads plus transactional writes. ctx.db is a DatabaseWriter (insert, patch, replace, delete on top of the reader surface), running inside one shard's transactional scope. Adds ctx.scheduler (enqueue deferred work) and ctx.workflows (start/inspect durable workflows), and upgrades ctx.vectors to the mutating VectorSearch. ctx.storage stays read-only; R2 writes are a side effect reserved for actions. A mutation can compose other mutations and queries (ctx.runMutation, ctx.runQuery), both reusing the same db writer and riding the same transaction — a composed mutation does not open a second one.

ctx.scheduler is transactional too: a runAfter/runAt issued inside a mutation is held until the mutation commits, and discarded if it rolls back. You still get the job id back synchronously, so you can store it on the row you just wrote and cancel it later.

ActionCtx

The escape hatch. An action can do anything a Worker can: it gets ctx.fetch (the standard fetch) for calling external APIs, the full read/write ctx.storage (store, delete, generateUploadUrl), ctx.scheduler, ctx.workflows, and the mutating ctx.vectors. It composes other functions of every kind: ctx.runQuery, ctx.runMutation, ctx.runAction. An action's ctx.db writes are not part of a single mutation transaction. Do transactional reads and writes inside a mutation and call it from the action: that composed mutation runs in its own all-or-nothing span, so a throw halfway through it rolls back every write it made — the action sees the error with nothing half-applied behind it.

Surfaces on every ctx

These are present regardless of kind:

  • ctx.auth: the authenticated identity. ctx.auth.userId is the verified user id or null; ctx.auth.getIdentity() resolves the full claim set. Populated by @lunora/auth when a session is present.
  • ctx.log: a structured, function-attributed logger (log, info, warn, error, debug). A drop-in for console, except each line is tagged with the function path and routed to your observability sink or the dev terminal.
  • ctx.ip: the caller's trusted CF-Connecting-IP, or undefined for a live-subscription re-run, a server-initiated dispatch, or non-Cloudflare hosting. Useful as a rate-limit key for anonymous traffic.
  • ctx.now: the wall-clock instant (epoch ms) the function began, captured once so the whole handler sees a single stable value. Read time from here in a query/mutation instead of Date.now(): those handlers must be deterministic (a query is re-run on every subscription re-evaluation), and the nondeterministic_query_mutation advisor flags Date.now(). Actions may use Date.now() freely but get ctx.now too for parity.

ctx field matrix

FieldQueryCtxMutationCtxActionCtx
dbDatabaseReaderDatabaseWriterDatabaseWriter*
auth
log
ip
now
storageread-onlyread-onlyread/write
vectorsread-onlyread/writeread/write
scheduler
workflows
fetch
runQuery
runMutation
runAction

* An action's ctx.db is not transactional; see above.

Add-on surfaces (codegen-wired)

Opt-in packages attach extra surfaces onto ctx through codegen. When a project uses one, the generated _generated/server.ts widens the context type so the surface is typed and present at runtime. Add-ons that wire a ctx surface include:

  • @lunora/aictx.ai (action context)
  • @lunora/flagsctx.flags (every context)
  • @lunora/bindings/analyticsctx.analytics
  • @lunora/bindings/kvctx.kv
  • @lunora/hyperdrivectx.sql (action context)
  • @lunora/browserctx.browser (action context)
  • @lunora/bindings/imagesctx.images
  • @lunora/containerctx.containers

The base QueryCtx / MutationCtx / ActionCtx in @lunora/server do not declare these; they appear only after codegen detects the add-on. Middleware can also augment ctx for downstream handlers (see Middleware).