BETTER-CONVEX

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

FeatureBenefit
Direct CallsNo HTTP roundtrip, direct function invocation
Type SafetyFull TypeScript inference for inputs and outputs
Auth IntegrationBuilt-in token management with Better Auth
Unified SyntaxSame 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.

.env.local
# WebSocket API (port 3210)
NEXT_PUBLIC_CONVEX_URL=http://localhost:3210

# HTTP routes (port 3211)
NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3211
.env.local
# 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.site

Create Caller

Without Auth

src/lib/convex/server.ts
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.

src/lib/convex/server.ts
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:

APILocationWhen to useTransport
createCallerFactorykitcn/serverServer-side HTTP calls (Next.js API routes, RSC)Convex fetch APIs
create<Module>Caller(ctx)convex/functions/generated/<module>.runtime.tsIn-process composition inside Convex handlersDirect 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.

convex/functions/teams.ts
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.

src/app/api/users/route.ts
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

src/middleware.ts
const ctx = await createContext({ headers: c.req.raw.headers });
const user = await ctx.caller.user.getSessionUser({});

API Routes

src/app/api/data/route.ts
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

Before (vanilla Convex)
src/app/api/users/route.ts
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' });
After (cRPC)
src/app/api/users/route.ts
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.isAuthenticated for auth state
  • createCallerFactory for reusable context creation
  • Built-in auth/token management

API Reference

Call Matrix

What each caller context can invoke via create<Module>Caller(ctx):

Caller contextRoot queryRoot mutationRoot actioncaller.actions.*caller.schedule.*
QueryCtx
MutationCtx
ActionCtx

createCallerFactory

Creates a caller factory with token management and retry logic.

types.ts
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 }:

types.ts
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>

Next Steps

On this page