Setup
Initialize cRPC and create procedure builders.
Generate Runtime
Start kitcn dev — this runs Convex and watches for changes, regenerating runtime files automatically:
npx kitcn devInitialize cRPC
<functionsDir>/generated/server.ts is the canonical server contract. It
exports initCRPC with ORM pre-wired when your default schema chains
.relations(...).
<functionsDir>/generated/auth.ts is the auth contract.
import { initCRPC } from '../functions/generated/server';
const c = initCRPC.create();Payload Transformer (Optional)
cRPC serializes inputs/outputs through a transformer at the procedure boundary.
Datesupport is always enabled (cannot be disabled)- The reserved wire tag is
$date - Pass a custom transformer only when you need extra wire-safe types
- Custom transformer is additive to the built-in Date transformer
import { createTaggedTransformer } from 'kitcn/crpc';
import { initCRPC } from '../functions/generated/server';
const transformer = createTaggedTransformer([
// Add custom codecs here (Date is already built-in)
]);
const c = initCRPC.create({ transformer });create({ transformer }) accepts either:
- A single
DataTransformer - A split
{ input, output }transformer object
Export Procedure Builders
Next, we'll export the procedure builders that you'll use throughout your codebase. These are the building blocks for all your queries, mutations, and actions.
// Public procedures - accessible from client
export const publicQuery = c.query;
export const publicMutation = c.mutation;
export const publicAction = c.action;
// Internal procedures - only callable from other backend functions
export const privateQuery = c.query.internal();
export const privateMutation = c.mutation.internal();
export const privateAction = c.action.internal();
// HTTP route builders (for REST APIs)
export const publicRoute = c.httpAction;
export const router = c.router;.internal() prevents client access — use for scheduled functions, webhooks, and server-to-server calls.
Define Your Schema
Before creating procedures, you'll need a database schema. These docs assume the ORM (see /docs/orm):
import {
convexTable,
defineSchema,
id,
index,
integer,
text,
} from 'kitcn/orm';
export const user = convexTable(
'user',
{ name: text().notNull(), email: text().notNull() },
(t) => [index('email').on(t.email)]
);
export const session = convexTable(
'session',
{
token: text().notNull(),
userId: id('user').notNull(),
expiresAt: integer().notNull(),
},
(t) => [index('token').on(t.token), index('userId').on(t.userId)]
);
export const tables = { user, session };
export default defineSchema(tables, {
strict: false,
}).relations((r) => ({
user: {
sessions: r.many.session({ from: r.user.id, to: r.session.userId }),
},
session: {
user: r.one.user({ from: r.session.userId, to: r.user.id }),
},
}));Without codegen, use initCRPC from kitcn/server with .dataModel() and optional .context().
Your First Procedure
import { query } from 'convex/server';
import { v } from 'convex/values';
export const list = query({
args: { limit: v.number() },
handler: async (ctx, args) => {
return ctx.db
.query('user')
.order('desc')
.take(args.limit);
},
});import { z } from 'zod';
import { publicQuery } from '../lib/crpc';
export const list = publicQuery
.input(z.object({ limit: z.number() }))
.query(async ({ ctx, input }) => {
return ctx.orm.query.user.findMany({ limit: input.limit });
});Procedure Types
| Type | Builder | Use For |
|---|---|---|
| Query | c.query | Read-only operations, real-time subscriptions |
| Mutation | c.mutation | Write operations, transactional updates |
| Action | c.action | Side effects, external API calls |
| HTTP Action | c.httpAction | REST endpoints, webhooks |
Adding Middleware
Define a middleware that checks auth and adds the user to context:
const authMiddleware = c.middleware(async ({ ctx, next }) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new CRPCError({ code: 'UNAUTHORIZED' });
}
const user = await ctx.orm.query.user.findFirst({
where: { email: identity.email! },
});
if (!user) throw new CRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, user } });
});Create authenticated procedure variants:
export const authQuery = c.query.use(authMiddleware);
export const authMutation = c.mutation.use(authMiddleware);ctx.user is now available in handlers:
export const me = authQuery
.output(userSchema)
.query(async ({ ctx }) => {
return ctx.user; // Typed! We know user exists
});Adding Metadata
const c = initCRPC
.meta<{
auth?: 'optional' | 'required';
role?: 'admin';
ratelimit?: string;
}>()
.create();Middleware reads metadata:
const roleMiddleware = c.middleware(({ ctx, meta, next }) => {
if (meta.role === 'admin' && !ctx.user?.isAdmin) {
throw new CRPCError({ code: 'FORBIDDEN' });
}
return next({ ctx });
});
export const adminQuery = c.query
.meta({ role: 'admin' })
.use(authMiddleware)
.use(roleMiddleware);Customizing Context
With generated initCRPC, ORM context is already wired. Chain .use() for additional context (e.g. auth middleware).
Common Procedure Variants
| Variant | Description | Middleware |
|---|---|---|
publicQuery | No auth required | None |
optionalAuthQuery | ctx.user may be null | Optional auth |
authQuery | ctx.user guaranteed | Auth required |
authMutation | Auth + rate limiting | Auth + rate limit |
adminQuery | Auth + admin role | Auth + role check |
For production bootstrap, start with CLI Registry. Use this page when you want to wire the backend by hand.
File Organization
Each file in convex/functions/ becomes a namespace:
convex/
├── functions/
│ ├── schema.ts # Database schema
│ ├── user.ts # → crpc.user.list, crpc.user.create
│ ├── session.ts # → crpc.session.getByToken
│ ├── account.ts # → crpc.account.list, crpc.account.delete
│ └── generated/
│ ├── server.ts # Generated server contract (ORM + initCRPC)
│ └── auth.ts # Generated auth contract
├── lib/
│ └── crpc.ts # Setup + procedure variants
└── shared/ # Client-importable
└── api.ts # Generated procedure metadataClient proxy mirrors this:
// File: convex/functions/user.ts, export: list
crpc.user.list.queryOptions({ limit: 10 })
// File: convex/functions/session.ts, export: getByToken
crpc.session.getByToken.queryOptions({ token: 'abc' })