Context
Access database, auth, and custom data in your procedures.
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).