Context
Access database, auth, and custom data in your procedures.
In this guide, we'll explore the context object - the gateway to everything your procedures need. You'll learn about base context properties, how to extend context with middleware, and how actions differ from queries and mutations.
Overview
Context (ctx) is passed to every procedure handler. It provides access to Convex features and any data added by middleware.
| Property | Available In | Purpose |
|---|---|---|
ctx.orm | Queries, Mutations | Database access (the ORM) |
ctx.auth | All | Authentication info |
ctx.storage | All | File storage |
ctx.scheduler | Mutations | Schedule functions |
ctx.runQuery / ctx.runMutation | Actions | Call other procedures |
Let's explore each one.
Base Context
Every procedure receives the base Convex context. Here's what you get out of the box:
export const list = publicQuery
.query(async ({ ctx }) => {
// ctx.orm - Database access (the ORM)
// ctx.auth - Authentication info
// ctx.storage - File storage
return ctx.orm.query.user.findMany({ limit: 100 });
});Database (ctx.orm)
The database is your primary way to read and write data. In queries, you can only read. In mutations, you can read and write.
import { user } from './schema';
// Read
const one = await ctx.orm.query.user.findFirst({ where: { id: id } });
const many = await ctx.orm.query.user.findMany({ limit: 100 });
// Write (mutations only)
await ctx.orm.insert(user).values({ name: 'John', email: 'john@example.com' });Authentication (ctx.auth)
Access the authenticated user's identity. This is the raw identity from your auth provider - use middleware to transform it into your app's user object.
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new CRPCError({ code: 'UNAUTHORIZED' });
}
// identity.subject - User ID from auth provider
// identity.email - Email (if available)
// identity.name - Name (if available)Tip: Rather than calling getUserIdentity() in every procedure, create an authQuery middleware that adds ctx.user automatically. See the next section.
Extending Context with Middleware
Use middleware to add custom data to context - the authenticated user, feature flags, rate limit info, anything your procedures need.
The pattern is simple: fetch what you need, then call next() with the extended context.
import { getSession } from 'kitcn/auth';
export const authQuery = c.query.use(async ({ ctx, next }) => {
const session = await getSession(ctx);
if (!session) {
throw new CRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}
const user = await ctx.orm.query.user.findFirst({
where: { id: session.userId },
});
if (!user) throw new CRPCError({ code: 'UNAUTHORIZED' });
return next({
ctx: { ...ctx, user: { id: user.id, session, ...user }, userId: user.id },
});
});
// Create mutation version with same pattern
export const authMutation = c.mutation.use(async ({ ctx, next }) => {
// ... same auth logic
});Now when you use authQuery or authMutation, ctx.user and ctx.userId are guaranteed to exist:
import { z } from 'zod';
import { authMutation } from '../lib/crpc';
import { session } from './schema';
export const create = authMutation
.input(z.object({ token: z.string() }))
.output(z.string())
.mutation(async ({ ctx, input }) => {
// ctx.user and ctx.userId are now available and typed!
const [row] = await ctx.orm
.insert(session)
.values({
...input,
userId: ctx.userId,
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
})
.returning({ id: session.id });
return row.id;
});Context in Actions
Actions have a different context. Instead of direct database access, they call other procedures. Use the module runtime caller (for example createUserCaller(ctx)) for type-safe dispatch with full autocomplete:
import { createUserCaller } from './generated/user.runtime';
export const processAndSave = publicAction
.input(z.object({ data: z.string() }))
.action(async ({ ctx, input }) => {
// External API call (only possible in actions)
const result = await fetch('https://api.example.com/process', {
method: 'POST',
body: JSON.stringify({ data: input.data }),
});
// Call a mutation to save results
const caller = createUserCaller(ctx);
await caller.updateProfile({ data: await result.json() });
await caller.actions.syncExternalAnalytics({ userId: 'u_1' });
await caller.schedule.now.sendFollowUpEmail({ userId: 'u_1' });
});Note: Actions can't access the database directly. Module runtime callers (like createUserCaller(ctx)) dispatch root calls via ctx.runQuery / ctx.runMutation, action calls via caller.actions.* (which uses ctx.runAction), and scheduling via caller.schedule.* (which uses ctx.scheduler).
Migrate from Convex
If you're coming from vanilla Convex or convex-helpers, here's what changes.
What stays the same
Base context properties work identically:
ctx.orm- Database access (queries + mutations)ctx.auth- Authenticationctx.storage- File storagectx.scheduler- Scheduler (mutations + actions)ctx.runQuery/ctx.runMutation/ctx.runAction- Call procedures (actions only)
What's new
cRPC's .use() middleware replaces convex-helpers' customQuery/customMutation:
import { customQuery, customCtx } from 'convex-helpers/server/customFunctions';
const userQuery = customQuery(query,
customCtx(async (ctx) => {
const user = await getUser(ctx);
if (!user) throw new Error('Authentication required');
return { user };
})
);const userQuery = c.query.use(async ({ ctx, next }) => {
const user = await getUser(ctx);
if (!user) throw new CRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, user } });
});Key differences:
- Call
next({ ctx })instead of returning context directly - Use
CRPCErrorfor typed errors - Chain multiple
.use()calls for composable middleware