Server Side Calls
Call procedures from server-side code without HTTP overhead.
This guide covers how to call cRPC procedures from server-side code. You'll learn to set up the caller factory, configure authentication, and invoke procedures directly without HTTP overhead.
Overview
| Feature | Benefit |
|---|---|
| Direct Calls | No HTTP roundtrip, direct function invocation |
| Type Safety | Full TypeScript inference for inputs and outputs |
| Auth Integration | Built-in token management with Better Auth |
| Unified Syntax | Same ctx.caller.x.y() pattern as client-side |
Let's explore the setup and usage patterns.
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 create<Module>Caller(ctx)
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>Caller(ctx) | convex/functions/generated/<module>.runtime.ts | In-process composition inside Convex handlers | Direct handler, runQuery/runMutation, runAction, and scheduler |
Use create<Module>Caller(ctx) when you're already inside a Convex handler and want to call other cRPC procedures with type-safe autocomplete — no need to manually choose between ctx.runQuery, ctx.runMutation, ctx.runAction, and ctx.scheduler.
import { z } from 'zod';
import { privateQuery } from '../lib/crpc';
import { createTeamsCaller } from './generated/teams.runtime';
export const getTeamAndOwner = privateQuery
.input(z.object({ teamId: z.string() }))
.query(async ({ ctx, input }) => {
const caller = createTeamsCaller(ctx);
const team = await caller.getTeam({ teamId: input.teamId });
const owner = await caller.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({});Migrate from Convex
What stays the same
- Server-side data fetching concept
- Full type safety for procedure calls
What's new
import { fetchQuery, fetchMutation } from 'convex/nextjs';
import { api } from '@convex/_generated/api';
const users = await fetchQuery(api.user.list, {});
const id = await fetchMutation(api.user.create, { name: 'John', email: 'john@example.com' });import { createContext } from '@/lib/convex/server';
const ctx = await createContext({ headers });
const users = await ctx.caller.user.list({});
const id = await ctx.caller.user.create({ name: 'John', email: 'john@example.com' });Key differences:
- Unified proxy syntax matching client-side usage
ctx.isAuthenticatedfor auth statecreateCallerFactoryfor reusable context creation- Built-in auth/token management
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 | ✅ | ✅ | ❌ | ✅ | ✅ |
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>