- name
- kitcn
- description
- Use for Convex/kitcn setup and feature work: cRPC, ORM, auth, React.
- sources
- ["www/content/docs/concepts.mdx","www/content/docs/orm/index.mdx","www/content/docs/orm/schema/relations.mdx","www/content/docs/orm/schema/triggers.mdx","www/content/docs/orm/queries/aggregates.mdx","www/content/docs/orm/queries/pagination.mdx","www/content/docs/server/error-handling.mdx","www/content/docs/server/http.mdx","www/content/docs/server/middlewares.mdx","www/content/docs/server/procedures.mdx","www/content/docs/server/server-side-calls.mdx","www/content/docs/react/queries.mdx","www/content/docs/react/mutations.mdx","www/content/docs/react/infinite-queries.mdx","www/content/docs/auth/client.mdx","www/content/docs/auth/server.mdx"]
# kitcn Core Skill (80% Path)
Use this file first for everyday feature delivery in an already configured kitcn app.
- If setup/bootstrap/env/auth wiring or project structure mirroring is missing, use `references/setup/index.md` (then the relevant setup file).
- If the task is advanced or niche, load only the specific feature reference listed at the end.
## Scope
In scope:
- Add or update schema tables, indexes, relations, and triggers.
- Implement cRPC procedures (`query`, `mutation`, `action`, `httpAction`) with runtime auth + rate limits.
- Implement feature UI with `useCRPC()` + TanStack Query.
- Add minimal high-value tests for auth, errors, and side effects.
Out of scope:
- Greenfield setup/install/env/bootstrap.
- Full plugin deep-dives (admin/organizations/polar).
- Internal package-level parity testing.
## Skill Contract
1. Favor `ctx.orm` for app data access.
2. Keep list/read paths bounded and index-aware.
3. Use cRPC builders and middleware; avoid raw handler objects for new feature code.
4. Use `CRPCError` for expected failures.
5. Prefer schema triggers for cross-row invariants, but move invariant maintenance to explicit mutation helpers if trigger execution is unstable (for example init/seed hangs or recursive write paths).
6. Keep auth/rate-limit checks server-side.
7. **Inter-procedure calls**: use generated runtime helpers: `create<Module>Handler(ctx)` in queries/mutations, `create<Module>Caller(ctx)` in actions/HTTP routes, `caller.actions.*` for action procedures, and `caller.schedule.*` for scheduling. Never call `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction` directly for module procedures.
## Shortcut Mode (tRPC + Drizzle Mental Model)
Default assumption:
- cRPC behavior is tRPC-like (builder chain + middleware + TanStack options).
- ORM behavior is Drizzle-like (schema, relations, `findMany/findFirst`, `insert/update/delete`).
Only remember these non-parity deltas:
1. Procedure input root must be `z.object(...)` (no primitive root args).
2. No `z.void()` outputs; omit `.output(...)` for no-value mutations.
3. `.output(...)` parses the handler's value as-is and substitutes nothing: a handler must return the schema's *input* type, so `z.string().nullable()` needs an explicit `null` (`?? null`), not `undefined`. Model absent values as `.nullable()`, never a top-level `.optional()` — Convex wires `undefined` as `null` and cannot express top-level optionality, so `.output(z.string().optional())` publishes `v.string()` and the deployment rejects the `null` whenever the handler returns `undefined`. `.optional()` inside an object is fine. The low-level `returns:` option on `zCustomQuery`/`zCustomMutation`/`zCustomAction` differs — it substitutes `null` for `undefined` before parsing.
4. Stacked `.input(...)` calls merge input shapes.
5. `.paginated({ limit, item })` must be before `.query()` and adds `input.cursor`, `input.endCursor`, and `input.limit`; pass all three to ORM cursor queries so live splits stay bounded.
6. Metadata is codegen’d onto `@convex/api` leaves (`api.namespace.fn.meta`) so never put secrets in `.meta(...)`; chaining `.meta(...)` is shallow merge and supports `defaultMeta`.
7. Auth metadata drives client behavior: `auth: "optional"` waits for auth load then runs, `auth: "required"` waits then skips when logged out.
8. `ctx.orm` enforces constraints + RLS; `ctx.db` bypasses them.
9. Non-paginated `findMany()` must be explicitly sized (`limit`, cursor mode, schema `defaultLimit`, or explicit `allowFullScan`).
10. Predicate `where` requires explicit `.withIndex(...)`; no implicit full scan fallback.
11. Cursor pagination uses the first `orderBy` field; index that field for stable paging.
12. `maxScan` applies to cursor mode only; `allowFullScan` is for non-cursor full-scan opt-in.
13. String operators / `columns` projection / many-relation subfilters can run post-fetch; bound result size early.
14. Search mode is relevance-ordered and does not support `orderBy`; vector mode has stricter limits (no cursor/offset/top-level where/order).
15. Update/delete without `where` throws unless `allowFullScan()`.
16. `count()`, `aggregate()`, and `groupBy()` require a matching `aggregateIndex`. Use `groupBy({ by, _count, _sum })` instead of multiple `.count()` calls or `findMany` + manual JS grouping. Every `by` field must be finite-constrained (`eq`/`in`/`isNull`) in `where`. See `references/features/aggregates.md`.
17. cRPC React queries are real-time by default (`subscribe: true`); never use `queryClient.invalidateQueries` for these subscribed paths.
18. In RSC, `prefetch` hydrates client, `caller` is server-only and not hydrated, `preloadQuery` hydrates but can cause stale split ownership if also rendered client-side.
19. Better Auth Next.js shortcut is `convexBetterAuth(...)`; generic server-only shortcut is `createCallerFactory(...)`.
20. On the kitcn auth client path, use `createAuthMutations(authClient)` wrappers so logout unsubscribes auth queries before sign out. Raw Convex preset keeps a smaller plain `authClient`.
21. **NEVER** use `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction` directly for module-to-module calls. Use the generated runtime helpers from `convex/functions/generated/<module>.runtime`.
22. **`create<Module>Handler(ctx)`** is the default in queries/mutations: zero overhead, query/mutation ctx only, and no redundant validation or middleware.
23. **`create<Module>Caller(ctx)`** is for actions and HTTP routes. Action procedures live under `caller.actions.*`; scheduling lives under `caller.schedule.now|after|at|cancel`. Use `requireActionCtx(ctx)` only for true `ActionCtx` callbacks; use `requireSchedulerCtx(ctx)` when mutation or action contexts can schedule. Each caller/handler eagerly loads its module, so split large modules.
24. API types (`Api`, `ApiInputs`, `ApiOutputs`, `Select`, `Insert`, `TableName`) import from `@convex/api` — no manual `inferApiInputs<typeof api>`.
25. HTTP router must export as `httpRouter` (not `appRouter`) for codegen.
26. Server wiring imports come from `convex/functions/generated/` directory: `getAuth`, `defineAuth` from `generated/auth`; `initCRPC`, `QueryCtx`, `MutationCtx`, `OrmCtx` from `generated/server`; `create<Module>Caller`, `create<Module>Handler` from `generated/<module>.runtime`. No manual `convex/lib/orm.ts`.
27. `defineAuth(() => ({ ...options, triggers }))` replaces split `getAuthOptions` + `authTriggers`. Trigger callbacks are doc-first: `beforeCreate(data)`, `onCreate(doc)`, `onUpdate(newDoc, oldDoc)` — no `ctx` first param.
28. Internal auth functions at `internal.generated.*` (not `internal.auth.*`).
29. Async mutation batching is the default (codegen wires it). Customize per call: `execute({ batchSize, delayMs })`. Opt into sync: `execute({ mode: 'sync' })` or `defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } })`. Relevant defaults: `mutationBatchSize`, `mutationLeafBatchSize`, `mutationMaxRows`, `mutationScheduleCallCap`.
30. Polymorphic unions are schema-first: use `actionType: discriminator({ variants, as? })` in `convexTable(...)`. Query config does not include a `polymorphic` option. Writes stay flat; reads synthesize nested `details` (or custom alias). Use `withVariants: true` to auto-load all `one()` relations on discriminator tables.
31. Do not add manual ORM mutation batching loops in app/plugin code by default. Convex runtime batching already handles mutation execution. Prefer set-based deletes/updates over per-row loops. Only add explicit chunking when batching external side effects (for example Resend API calls) or bounded cleanup sweeps.
## Directory Boundary
Use `references/setup/` when the task needs:
1. Project/file structure setup → `setup/index.md` + `setup/server.md`
2. Auth bootstrap → `setup/auth.md`
3. Client/provider wiring → `setup/react.md`
4. Framework-specific setup → `setup/next.md` or `setup/start.md`
For full template-level recreation: start with `setup/index.md`, then load relevant setup files, then load selected feature refs.
## First-Pass Feature Intake (Do This Before Edits)
Lock these decisions first:
1. Auth level per endpoint: `public` / `optionalAuth` / `auth` / `private`.
2. Data invariants: what must always be true after writes?
3. Query shape: list, detail, relation-loaded, search, or stream composition.
4. Pagination mode: offset, cursor, infinite.
5. Side effects: trigger vs scheduled function vs inline mutation.
6. UI consumption: client hook only, RSC prefetch, or server-only caller.
7. Risk paths: unauthorized, forbidden, not found, conflicts, rate limit.
## Canonical File Targets
Typical feature touches:
- `convex/functions/schema.ts`
- `convex/functions/<feature>.ts`
- `convex/lib/crpc.ts` (only if middleware/procedure builder changes)
- `src/lib/convex/crpc.tsx` (only if cRPC context/meta wiring changes)
- `src/**` feature UI files
- `convex/functions/http.ts` or `convex/routers/**` for HTTP endpoints
- `convex/functions/crons.ts` or scheduled handlers if needed
## E2E Build Order (Default)
1. Schema + indexes + relations.
2. Trigger hooks for cross-row invariants (or explicit mutation-side sync if trigger path is unstable).
3. Procedures with strict input/output + auth + rate limits.
4. React hooks (query/mutation/infinite) using cRPC options.
5. Optional: HTTP route(s), scheduling hooks.
6. Tests for auth/error/trigger behavior.
## Core Patterns
### 1) Schema + Relations + Trigger
```ts
import {
convexTable,
defineSchema,
id,
integer,
index,
text,
timestamp,
} from "kitcn/orm";
export const project = convexTable(
"project",
{
name: text().notNull(),
ownerId: id("user").notNull(),
updatedAt: timestamp()
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
},
(t) => [index("ownerId_updatedAt").on(t.ownerId, t.updatedAt)]
);
export const task = convexTable(
"task",
{
projectId: id("project").notNull(),
title: text().notNull(),
status: text().notNull().default("open"),
updatedAt: timestamp()
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
},
(t) => [index("projectId_updatedAt").on(t.projectId, t.updatedAt)]
);
export default defineSchema({ project, task })
.relations((r) => ({
project: {
tasks: r.many.task(),
},
task: {
project: r.one.project({ from: r.task.projectId, to: r.project.id }),
},
}))
.triggers({
task: {
change: async (change, ctx) => {
const projectId = change.newDoc?.projectId ?? change.oldDoc?.projectId;
if (!projectId) return;
const open = await ctx.orm.query.task.findMany({
where: { projectId, status: "open" },
columns: { id: true },
limit: 500,
});
await ctx.orm.update(project).set({ openTaskCount: open.length });
},
},
});
```
Schema rules that matter:
1. Index fields that power filters/order/search.
2. `many()` relation paths need child FK indexes.
3. Trigger logic must be bounded and non-recursive.
4. Use table defaults for consistent write behavior.
5. Keep full ORM/query edge cases in `references/features/orm.md`.
### 2) Procedure Builders + Middleware
```ts
import { getSession } from "kitcn/auth";
import { CRPCError } from "kitcn/server";
import { initCRPC, type QueryCtx } from "../functions/generated/server";
const c = initCRPC
.meta<{
auth?: "optional" | "required";
role?: "admin";
ratelimit?: string;
}>()
.create();
function requireAuth<T>(user: T | null): T {
if (!user) {
throw new CRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
}
return user;
}
async function getSessionUser(ctx: QueryCtx) {
const session = await getSession(ctx);
if (!session) return null;
return await ctx.orm.query.user.findFirst({
where: { id: { eq: session.userId } },
});
}
export const publicQuery = c.query.meta({ auth: "optional" });
export const authQuery = c.query
.meta({ auth: "required" })
.use(async ({ ctx, next }) => {
const user = requireAuth(await getSessionUser(ctx));
return next({ ctx: { ...ctx, user, userId: user.id } });
});
export const authMutation = c.mutation
.meta({ auth: "optional" })
.use(async ({ ctx, next }) => {
const user = await getSessionUser(ctx);
return next({
ctx: { ...ctx, user, userId: user?.id ?? null },
});
});
```
Builder rules that matter:
1. Build `public`, `optional`, `auth`, and `private` procedure families once in `convex/lib/crpc.ts`. Authenticated action builders live in `convex/lib/crpc-action.ts`, the only builder module that imports `getAuth`.
2. `.meta(...)` is client-visible via generated API metadata. Never put secrets there.
3. Middleware receives server-only `procedure` info. When procedures are built from your app `generated/server` helper, standard `export const` queries, mutations, and actions infer `module:function` automatically from file path + export name. Use `.name("module:function")` only to override or cover unusual export shapes.
4. Resolve session/user once in middleware. Do not re-fetch auth state in every procedure.
Query/mutation middleware uses `getSession(ctx)` from `kitcn/auth`, which reads the session row
directly. Keep `getAuth(ctx)` out of `convex/lib/crpc.ts`: it pulls the whole Better Auth
definition and every auth plugin into the static import closure of every procedure module, and
Convex has no dynamic `import()` to escape it. Import `getAuth` only in the modules that call
`auth.api.*` — `convex/lib/crpc-action.ts`, HTTP routes, and organization/admin mutations.
View on GitHub