Concepts
Architecture and vocabulary for kitcn.
| Concept | What It Does |
|---|---|
| cRPC Builder | tRPC-style fluent API for defining procedures |
| Real-time + TanStack Query | Built-in adapter bridges Convex subscriptions into TanStack Query |
| Context & Middleware | Composable layers for auth, rate limiting, custom logic |
| Execution Model | Queries, mutations, actions, and scheduling |
| Database Layer | ORM relations, triggers, aggregates, migrations |
| Plugins | Cross-cutting features that span schema, runtime, and client |
| File Structure | Organized functions/, lib/, shared/ directories |
The Problem
Convex is reactive and everything syncs in real-time. But as your app grows, friction builds up — manually wiring TanStack Query hooks, copy-pasting auth checks into every function, adding rate limiting boilerplate.
kitcn solves this. Convex's reactive database + TanStack Query's DX:
- tRPC-style API — Fluent builder with full type inference
- TanStack Query Native —
useQuery,useMutation, DevTools - Real-time by Default — WebSocket subscriptions, no extra setup
- End-to-end Type Safety — Schema to components, everything typed
- Middleware Chains — Auth, rate limiting, custom context
cRPC Builder
Chain methods to build procedures with validation, middleware, and type inference:
export const list = authQuery
.input(z.object({ limit: z.number().optional() }))
.query(async ({ ctx, input }) => {
return ctx.orm.query.todos.findMany({ limit: input.limit ?? 10 });
});authQuery is a middleware-powered builder — auth runs before your handler, adds user to context, and every procedure using it gets that automatically. No copy-pasting auth checks.
Real-time + TanStack Query
Convex already has real-time subscriptions. kitcn bridges them into TanStack Query so you get WebSocket updates and the full TanStack Query API — isPending, isError, DevTools, cache invalidation, optimistic updates — without manual wiring.
With vanilla Convex, you'd use @convex-dev/react-query and wire each query with convexQuery(api.path, args). kitcn includes the adapter out of the box and generates typed query options from your procedures:
const { data, isPending } = useQuery(crpc.todos.list.queryOptions({}));One line: fetches data, subscribes to WebSocket updates, caches in TanStack Query. No adapter setup, no manual query key management.
For a detailed feature-by-feature comparison with vanilla Convex, see Comparison.
Context & Middleware
Every procedure receives ctx. Middleware transforms it before your handler runs:
import type { GenericId } from 'convex/values';
import { CRPCError } from 'kitcn/server';
const authQuery = c.query.use(async ({ ctx, next }) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new CRPCError({ code: 'UNAUTHORIZED' });
const userId = identity.subject as GenericId<'user'>;
const user = await ctx.orm.query.user.findFirst({ where: { id: userId } });
if (!user) throw new CRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, user, userId: user.id } });
});Define once, use everywhere. Need admin-only? Extend authQuery with a role check.
Execution Model
Convex has three procedure types with distinct runtime behaviors:
| Type | Best For | Key Behavior |
|---|---|---|
query | Read paths, reactive UI | Reactive subscriptions, no side effects |
mutation | Writes, transactions | Atomic, retried on internal failures |
action | External APIs, side effects | Not retried, can call external services |
Mutations and actions can schedule future work via caller.schedule.after(delayMs), caller.schedule.at(timestamp), or caller.schedule.now. Cron jobs handle recurring schedules. See Scheduling.
Most Convex bugs come from picking the wrong type. Start with query, move to mutation for writes, action only for external side effects.
Database Layer
ORM Relations
const user = await ctx.orm.query.user.findFirst({
where: { id: userId },
with: { posts: { limit: 10, orderBy: { createdAt: 'desc' } } },
});Triggers
Automatic side effects on data changes — denormalized counts, cascade deletes, audit logging:
import { defineTriggers, eq } from 'kitcn/orm';
import { post, relations } from './schema';
const triggers = defineTriggers(relations, {
user: {
change: async (change, ctx) => {
if (change.operation === 'delete') {
const posts = await ctx.orm.query.post.findMany({
where: { authorId: change.id },
limit: 100,
});
for (const postRow of posts) {
await ctx.orm.delete(post).where(eq(post.id, postRow.id));
}
}
},
},
});Aggregates
O(log n) counts, sums, and rankings:
const count = await ctx.orm.query.user.count();
const stats = await ctx.orm.query.user.aggregate({
where: { status: 'active' },
_sum: { amount: true },
_avg: { amount: true },
});
// Ranked access via rankIndex
const lb = ctx.orm.query.scores.rank('leaderboard', { where: { gameId } });
const top10 = await lb.paginate({ cursor: null, limit: 10 });Migrations
Built-in ORM migrations for safe schema evolution:
import { defineMigration, defineMigrationSet } from 'kitcn/orm';
const addStatus = defineMigration({
id: '20260214_add_status',
table: 'projects',
up: async ({ db }) => {
// backfill logic
},
});
export const migrations = defineMigrationSet([addStatus]);See ORM Migrations for the full workflow.
Plugins
Cross-cutting features that span schema, runtime, and client. Register once, everything wired:
import { defineSchema } from 'kitcn/orm';
import { ratelimitExtension } from '../lib/plugins/ratelimit/schema';
export default defineSchema(tables).extend(ratelimitExtension());See Plugins and Rate Limiting.
File Structure
convex/
├── functions/ # Convex functions (deployed)
│ ├── _generated/ # api.ts, dataModel.ts (auto-generated)
│ ├── schema.ts # Database schema definition
│ ├── user.ts # User procedures
│ └── todos.ts # Todo procedures
├── lib/ # Shared helpers (not deployed)
│ ├── crpc.ts # cRPC builder and middleware
│ ├── orm.ts # ORM context attachment
│ └── plugins/
│ └── ratelimit/
│ └── plugin.ts # Rate limiting policy + middleware
└── shared/ # Client-importable code
├── types.ts # Shared TypeScript types
└── api.ts # Generated procedure metadataGenerated Artifacts
| Path | Purpose |
|---|---|
convex/functions/_generated/* | Convex API and data model types |
convex/functions/generated/* | kitcn callers and runtime contracts |
convex/shared/api.ts | Typed API metadata for the client |
Dev loop: change schema/procedures → kitcn dev → use generated artifacts.
Configuration
{
"functions": "convex/functions",
"codegen": {
"staticApi": true,
"staticDataModel": true
},
"typescriptCompiler": "tsgo"
}| Option | Purpose |
|---|---|
functions | Directory containing your Convex functions |
codegen.staticApi | Generate static API types for better inference |
codegen.staticDataModel | Generate static data model types |
typescriptCompiler | Use "tsgo" for native TypeScript 7 support |