Server Side Calls
Call procedures from server-side code without HTTP overhead.
Environment Setup
The caller requires the Convex site URL (for HTTP actions), which is different from the real-time URL.
Note: The site URL uses port 3211 locally and .site domain in production.
# WebSocket API (port 3210)
NEXT_PUBLIC_CONVEX_URL=http://localhost:3210
# HTTP routes (port 3211)
NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3211# Generated by Convex
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Add manually - replace .cloud with .site
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-project.convex.siteCreate Caller
Without Auth
import { api } from '@convex/api';
import { createCallerFactory } from 'kitcn/server';
export const { createContext, createCaller } = createCallerFactory({
api,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});Better Auth (Next.js)
For Better Auth with Next.js, see Next.js Setup which uses convexBetterAuth from kitcn/auth/nextjs.
Custom Auth
Use createCallerFactory with a custom getToken function.
import { api } from '@convex/api';
import { createCallerFactory } from 'kitcn/server';
export const { createContext, createCaller } = createCallerFactory({
api,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
auth: {
getToken: async (siteUrl, headers) => {
// Extract token from headers (implement your auth logic)
const token = headers.get('authorization')?.replace('Bearer ', '');
return { token };
},
},
});Note: The generated api object already includes cRPC metadata. No separate meta parameter is needed.
If you customize transformer, use the same transformer in:
initCRPC.create({ transformer })createCRPCContext({ transformer })createCallerFactory({ transformer })getServerQueryClientOptions({ transformer })
Date support stays always on; custom transformer is additive only.
createCallerFactory vs generated runtime factories
There are two caller APIs, each for a different context:
| API | Location | When to use | Transport |
|---|---|---|---|
createCallerFactory | kitcn/server | Server-side HTTP calls (Next.js API routes, RSC) | Convex fetch APIs |
create<Module>Handler(ctx) | convex/functions/generated/<module>.runtime.ts | Query/mutation composition inside Convex handlers | Direct handler |
create<Module>Caller(ctx) | convex/functions/generated/<module>.runtime.ts | Action/HTTP composition inside Convex handlers | runQuery, runMutation, runAction, and scheduler |
Use create<Module>Handler(ctx) as the default inside queries and mutations.
Use create<Module>Caller(ctx) in actions and HTTP routes, where handler
factories are unavailable.
import { z } from 'zod';
import { privateQuery } from '../lib/crpc';
import { createTeamsHandler } from './generated/teams.runtime';
export const getTeamAndOwner = privateQuery
.input(z.object({ teamId: z.string() }))
.query(async ({ ctx, input }) => {
const handler = createTeamsHandler(ctx);
const team = await handler.getTeam({ teamId: input.teamId });
const owner = await handler.getOwner({ id: team.ownerId });
return { team, owner };
});Note: In actions, each create<Module>Caller(ctx) call runs as a separate Convex transaction. Prefer aggregating related reads/writes into a single internal query/mutation when consistency matters.
For the full call matrix and type definitions, see API Reference below. For more details on composition, see In-Process Procedure Composition.
Usage
Create context from headers, then use ctx.caller.
import { createContext } from '@/lib/convex/server';
// Create context from request headers
const ctx = await createContext({ headers: request.headers });
// Query
const users = await ctx.caller.user.list({});
// Query with input
const user = await ctx.caller.user.getById({ id: userId });
// Mutation
const newId = await ctx.caller.user.create({
name: 'John',
email: 'john@example.com',
});
// Check auth state
if (!ctx.isAuthenticated) {
redirect('/login');
}Hono Middleware
const ctx = await createContext({ headers: c.req.raw.headers });
const user = await ctx.caller.user.getSessionUser({});API Routes
const ctx = await createContext({ headers: request.headers });
const data = await ctx.caller.user.list({});API Reference
Call Matrix
What each caller context can invoke via create<Module>Caller(ctx):
| Caller context | Root query | Root mutation | Root action | caller.actions.* | caller.schedule.* |
|---|---|---|---|---|---|
QueryCtx | ✅ | ❌ | ❌ | ❌ | ❌ |
MutationCtx | ✅ | ✅ | ❌ | ❌ | ✅ |
ActionCtx | ✅ | ✅ | ❌ | ✅ | ✅ |
If ctx is already typed as ActionCtx, call the generated caller directly.
Use caller.actions.* only from a real action context.
If the flow can run from a mutation or generic scheduler-capable callback, do
not force requireActionCtx(ctx) just to reach an action. Schedule it instead:
import { requireSchedulerCtx } from 'kitcn/server';
const caller = createJobsCaller(requireSchedulerCtx(ctx));
await caller.schedule.now.reindex({ force: true });Only use requireActionCtx(ctx) when the callback truly runs in ActionCtx
and you need caller.actions.*.
createCallerFactory
Creates a caller factory with token management and retry logic.
type CreateCallerFactoryOptions<TApi> = {
api: TApi;
convexSiteUrl: string;
auth?: {
getToken: (
siteUrl: string,
headers: Headers,
opts?: unknown
) => Promise<{ token?: string; isFresh?: boolean }>;
isUnauthorized?: (error: unknown) => boolean;
};
// Additive only. Built-in Date transformer remains enabled.
transformer?: DataTransformer | { input: DataTransformer; output: DataTransformer };
};Returns { createContext, createCaller }:
type ConvexContext<TApi> = {
token: string | undefined;
isAuthenticated: boolean;
caller: ServerCaller<TApi>;
};
// createContext: (opts: { headers: Headers }) => Promise<ConvexContext<TApi>>
// createCaller: (ctxFn: () => Promise<ConvexContext<TApi>>) => LazyCaller<TApi>