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
- Favor
ctx.orm for app data access.
- Keep list/read paths bounded and index-aware.
- Use cRPC builders and middleware; avoid raw handler objects for new feature code.
- Use
CRPCError for expected failures.
- 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).
- Keep auth/rate-limit checks server-side.
- 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:
- Procedure input root must be
z.object(...) (no primitive root args).
- No
z.void() outputs; omit .output(...) for no-value mutations.
.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.
- Stacked
.input(...) calls merge input shapes.
.paginated({ limit, item }) must be before .query() and auto-adds input.cursor + input.limit, output { page, continueCursor, isDone }.
- 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.
- Auth metadata drives client behavior:
auth: "optional" waits for auth load then runs, auth: "required" waits then skips when logged out.
ctx.orm enforces constraints + RLS; ctx.db bypasses them.
- Non-paginated
findMany() must be explicitly sized (limit, cursor mode, schema defaultLimit, or explicit allowFullScan).
- Predicate
where requires explicit .withIndex(...); no implicit full scan fallback.
- Cursor pagination uses the first
orderBy field; index that field for stable paging.
maxScan applies to cursor mode only; allowFullScan is for non-cursor full-scan opt-in.
- String operators /
columns projection / many-relation subfilters can run post-fetch; bound result size early.
- Search mode is relevance-ordered and does not support
orderBy; vector mode has stricter limits (no cursor/offset/top-level where/order).
- Update/delete without
where throws unless allowFullScan().
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.
- cRPC React queries are real-time by default (
subscribe: true); never use queryClient.invalidateQueries for these subscribed paths.
- 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.
- Better Auth Next.js shortcut is
convexBetterAuth(...); generic server-only shortcut is createCallerFactory(...).
- 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.
- 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.
create<Module>Handler(ctx) is the default in queries/mutations: zero overhead, query/mutation ctx only, and no redundant validation or middleware.
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.
- API types (
Api, ApiInputs, ApiOutputs, Select, Insert, TableName) import from @convex/api — no manual inferApiInputs<typeof api>.
- HTTP router must export as
httpRouter (not appRouter) for codegen.
- 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.
defineAuth(() => ({ ...options, triggers })) replaces split getAuthOptions + authTriggers. Trigger callbacks are doc-first: beforeCreate(data), onCreate(doc), onUpdate(newDoc, oldDoc) — no ctx first param.
- Internal auth functions at
internal.generated.* (not internal.auth.*).
- 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.
- 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.
- 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:
- Project/file structure setup →
setup/index.md + setup/server.md
- Auth bootstrap →
setup/auth.md
- Client/provider wiring →
setup/react.md
- 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:
- Auth level per endpoint:
public / optionalAuth / auth / private.
- Data invariants: what must always be true after writes?
- Query shape: list, detail, relation-loaded, search, or stream composition.
- Pagination mode: offset, cursor, infinite.
- Side effects: trigger vs scheduled function vs inline mutation.
- UI consumption: client hook only, RSC prefetch, or server-only caller.
- 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)
- Schema + indexes + relations.
- Trigger hooks for cross-row invariants (or explicit mutation-side sync if trigger path is unstable).
- Procedures with strict input/output + auth + rate limits.
- React hooks (query/mutation/infinite) using cRPC options.
- Optional: HTTP route(s), scheduling hooks.
- Tests for auth/error/trigger behavior.
Core Patterns
1) Schema + Relations + Trigger
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:
- Index fields that power filters/order/search.
many() relation paths need child FK indexes.
- Trigger logic must be bounded and non-recursive.
- Use table defaults for consistent write behavior.
- Keep full ORM/query edge cases in
references/features/orm.md.
2) Procedure Builders + Middleware
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:
- 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.
.meta(...) is client-visible via generated API metadata. Never put secrets there.
- 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.
- 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.
- Shared
c.middleware() chains preserve mutation writer types on mutation procedures. If the middleware itself performs writes, type it as mutation-only with c.middleware<MutationCtx>(...).
- Keep deeper auth/runtime edge cases in
references/setup/server.md and references/features/auth*.md.
3) Query + Mutation Procedure Template
import * as z from "zod";
import { eq } from "kitcn/orm";
import { CRPCError } from "kitcn/server";
import { authMutation, authQuery } from "../lib/crpc";
import { project } from "./schema";
export const listProjects = authQuery
.paginated({ limit: z.number().min(1).max(50).default(20), item: project })
.query(async ({ ctx, input }) =>
ctx.orm.query.project.findMany({
where: { ownerId: ctx.userId },
orderBy: { updatedAt: "desc" },
cursor: input.cursor,
limit: input.limit,
})
);
export const renameProject = authMutation
.input(z.object({ id: z.string(), name: z.string().min(1).max(120) }))
.mutation(async ({ ctx, input }) => {
const current = await ctx.orm.query.project.findFirst({
where: { id: input.id, ownerId: ctx.userId },
columns: { id: true },
});
if (!current) {
throw new CRPCError({ code: "NOT_FOUND", message: "Project not found" });
}
await ctx.orm
.update(project)
.set({ name: input.name })
.where(eq(project.id, current.id));
return null;
});
Procedure rules that matter:
- Root input must be
z.object(...).
- Use strict
.input(...); add .output(...) only when needed.
- Omit
.output(...) for no-value mutations.
- Use the default mutation rate limit; add
.meta({ ratelimit: ... }) only for named bucket overrides.
- Throw
CRPCError for expected outcomes.
- Bound every list with
limit, cursor, or .paginated(...).
- Move advanced query-builder shapes to
references/features/orm.md.
3b) Inter-Procedure Composition
Use:
create<Module>Handler(ctx) in queries/mutations.
create<Module>Caller(ctx) in actions/HTTP routes.
caller.actions.* for action procedures.
caller.schedule.* for scheduled procedures.
- Never
ctx.runQuery / ctx.runMutation / ctx.runAction for module procedures.
4) Query Modes (Use The Right One)
- Default to object
where.
- Use callback
where only when composition reads better than object form.
- Predicate/filter callbacks require
.withIndex(...) first plus explicit limit/maxScan.
- Full-text search uses
search: { index, query, filters } and does not support orderBy.
- Cursor paging is only stable when the
orderBy field is indexed.
- Advanced modes (
pageByKey, vector search, pipelines, aggregate indexes) live in references/features/orm.md.
5) Mutation Patterns (Most Used)
- Use
.returning(...) on inserts when caller needs created ids.
- Every update/delete path gets an explicit
where(...).
- Clear optional columns with
unsetToken.
- Async mutation execution is the default; use
.execute({ mode: "sync" }) only when atomic all-at-once behavior is required.
- Prefer set-based deletes/updates. Add chunking only for external side effects or bounded cleanups.
- Upsert, conflict handling, mutation batching, and schema extension edge cases live in
references/features/orm.md.
6) Error Model
Use this map consistently:
BAD_REQUEST: invalid input or business precondition.
UNAUTHORIZED: no session.
FORBIDDEN: session exists, permission missing.
NOT_FOUND: missing or inaccessible resource.
CONFLICT: duplicate or conflicting write.
TOO_MANY_REQUESTS: rate limit.
INTERNAL_SERVER_ERROR: unexpected failures only. cRPC also raises it for a
failed .output(...) parse, with message Output validation failed and
sanitized structural Zod issues in error.data.ZodError. Custom issue
messages and fields stay server-side because they can contain handler output.
- Add small custom
data payloads on CRPCError when the client needs
domain metadata like conflicting ids. Read them on the client from
error.data.
Required tests:
- unauthenticated rejection
- permission rejection when relevant
- missing resource path
- conflict path when relevant
- rate-limited write path when relevant
7) React Query Integration
Preconditions (must be true before writing/using useCRPC() code paths):
- Generated imports exist (
@convex/api) from setup bootstrap.
- Provider chain is mounted (
CRPCProvider inside QueryClient + Convex provider flow).
- If bootstrap/provider prerequisites are missing, stop feature work and finish
references/setup/ first.
- Backend state is project-local in
.convex/, not ~/.convex.
useCRPC() pattern: const crpc = useCRPC(); const projects = useQuery(crpc.project.listProjects.queryOptions({ cursor: null, limit: 20 })); const createProject = useMutation(crpc.project.createProject.mutationOptions());
Key client defaults/deltas:
- Queries are real-time by default (
subscribe: true).
- Never use
queryClient.invalidateQueries for subscribed cRPC query paths.
- Use
{ subscribe: false } only for one-time fetches; refresh those with explicit refetch/fetchQuery.
- Use
skipUnauth: true to avoid unauthorized fetch churn.
- For pagination, use
useInfiniteQuery from kitcn/react.
- Prefer typed
queryKey(...) helpers for cache read/write/fetch ops instead of manual keys.
- For kitcn auth flows, prefer
createAuthMutations(...) wrappers (not raw auth client calls) to avoid logout race errors. Raw Convex preset keeps the plain auth client path.
- For mutation toasts, prefer
error.data?.message over error.message; data.message is the clean CRPCError payload.
- Prefer one global
QueryClient mutation onError toast with mutation.meta.errorMessage / skipErrorToast rather than copy-pasting onError in every component.
- Full client/RSC depth lives in
references/features/react.md.
8) RSC Patterns (Next.js)
Choose one per use case:
prefetch(...) (preferred): non-blocking, hydrated, client owns data.
caller.*: blocking server-only logic (redirects/auth checks), not hydrated.
preloadQuery(...): blocking + hydrated when server needs data immediately.
Do not render preloadQuery result on server and again on client for the same data path.
HydrateClient must wrap all client components that consume prefetched queries.
- Next.js-specific setup and deeper hydration tradeoffs live in
references/setup/next.md and references/features/react.md.
9) HTTP Route Pattern (When Feature Needs REST/Webhooks)
import { createTaskCaller } from "../functions/generated/task.runtime";
export const createTaskRoute = authRoute
.post("/api/projects/:projectId/tasks")
.params(z.object({ projectId: z.string() }))
.input(z.object({ title: z.string().min(1) }))
.output(z.object({ id: z.string() }))
.mutation(async ({ ctx, params, input }) => {
const caller = createTaskCaller(ctx);
const id = await caller.createFromHttp({
projectId: params.projectId,
title: input.title,
userId: ctx.userId,
});
return { id };
});
HTTP-specific rules:
- Use
z.coerce.* for search params.
- Keep auth and permission checks in middleware/procedure.
- Apply rate limits to public/heavy endpoints.
- Validate webhook signatures before any side effects.
- Use
publicRoute / authRoute / optionalAuthRoute builders from convex/lib/crpc.ts.
- Compose endpoints with
router(...) for feature-level HTTP grouping.
- Client calls must pass path/query args as
{ params, searchParams }; query values are strings.
- Webhooks, streaming, and Hono-specific patterns live in
references/features/http.md.
10) Scheduling Pattern (If Needed)
Example: const caller = createTaskCaller(ctx); await caller.schedule.now.sendTaskCreated({ taskId: created.id, userId: ctx.userId }); await caller.schedule.at(input.sendAt).sendReminder({ taskId: input.taskId, userId: ctx.userId });
Scheduling rules:
- Auth context is not propagated; pass user/org IDs explicitly.
- Mutation scheduling is atomic with the mutation transaction.
- Store returned job IDs when cancellation is required.
- Scheduling inside actions is not atomic with action failure.
- Cron schedules run in UTC.
- Use
ctx.scheduler.* directly only when you must schedule non-procedure internal.* functions.
- Cron expressions and operational details live in
references/features/scheduling.md.
11) Testing Baseline (High Signal)
Minimum feature test set:
- happy path query/mutation
- unauthenticated rejection (
UNAUTHORIZED)
- permission/ownership rejection (
FORBIDDEN where relevant)
- missing resource (
NOT_FOUND)
- trigger side effect assertion
- scheduler assertion if feature schedules work
- not-found checks should use real IDs or non-ID lookup keys (slug/name/email), not synthetic IDs
- Full testing recipes live in
references/features/testing.md.
- If Convex bootstrap blocks integration tests, extract pure guards/helpers and keep one smoke integration test once bootstrap works.
Performance + Safety Checklist
Before calling a feature done:
- Every list query is bounded (
limit/cursor).
- Filters/order align with indexes.
- Expensive post-fetch logic uses pre-narrowed index path.
- Mutations use targeted
where and avoid accidental full scans.
- Trigger logic is bounded, idempotent, and avoids ping-pong loops.
- Error codes are explicit and intentional.
- User-facing writes have rate-limit metadata.
- Tests cover auth + not-found + side effects.
ctx.db is not used on paths that rely on ORM constraints/RLS.
- Paginated endpoints use
.paginated(...) + ORM cursor flow (not ad-hoc wrappers).
- For any predicate/full-scan-like path,
.withIndex(...) + bound (limit/maxScan) is explicit.
- NEVER use
@ts-nocheck, no global lint-rule downgrades, no unresolved lint warnings in touched files.
Common Mistakes (And Fixes)
| Mistake |
Correct pattern |
| Raw Convex handler for new feature procedures |
cRPC builders (publicQuery, authMutation, etc.) |
| Write-time side effects duplicated across mutations |
Schema trigger, or one centralized mutation-side sync helper when trigger path is unsafe |
| Missing bounds on list/search |
Add limit + cursor/pagination |
orderBy written as array objects |
Use object form: orderBy: { updatedAt: "desc" } |
Using ctx.db for policy-sensitive reads |
Use ctx.orm (RLS/constraints path) |
Throwing generic Error for expected outcomes |
Throw CRPCError with explicit code |
| Infinite list with TanStack native hook directly |
Use useInfiniteQuery from kitcn/react |
Primitive root input (z.string()) |
Use root z.object(...) input schema |
Returning nothing with z.void() |
Omit explicit output |
Returning a possibly-missing lookup under .output(...nullable()) |
Coalesce it: ?? null. .output(...) substitutes nothing for undefined |
| Manual pagination wrappers for infinite endpoints |
Use .paginated({ limit, item }) |
Synthetic Convex IDs in tests ("missing-id") |
Use inserted IDs or semantic lookup keys |
| Aggregates disabled but helper/config still present |
Remove aggregate helper + defineTriggers handlers + app config together |
Putting secrets in .meta(...) |
Keep metadata non-sensitive (client-visible) |
Using ctx.runQuery/ctx.runMutation/ctx.runAction directly |
Use create<Module>Handler(ctx) in queries/mutations, create<Module>Caller(ctx) in actions/HTTP with caller.actions.* / caller.schedule.* (from generated/<module>.runtime) |
Using createCaller in query/mutation context |
Use create<Module>Handler(ctx) — zero overhead, bypasses redundant validation |
Adding // @ts-nocheck to unblock compile |
NEVER do this; fix the underlying types using canonical patterns in references/setup/ |
| Relaxing lint rules to pass checks |
Keep baseline lint config; fix code-level warnings/errors instead |
Reference Escalation Map (Load Only If Needed)
Setup (once per project):
references/setup/index.md: bootstrap, env, decision intake, gates, checklist, troubleshooting
references/setup/server.md: core backend (schema, ORM, cRPC) + optional module gates
references/setup/auth.md: auth core bootstrap + plugin setup
references/setup/react.md: client core (QueryClient, provider, cRPC context)
references/setup/next.md: Next.js App Router setup
references/setup/start.md: TanStack Start setup
references/setup/doc-guidelines.md: skill/docs sync contract
Features (per session, self-contained):
references/features/orm.md: full ORM API, constraints, RLS, advanced mutations, filtering/search/composition/pagination
references/features/react.md: full client, RSC, hydration, error handling matrix
references/features/http.md: typed REST routes, webhooks, streaming
references/features/scheduling.md: cron + delayed job patterns
references/features/testing.md: deeper testing scenarios
references/features/aggregates.md: aggregate component patterns
references/features/migrations.md: built-in online data migrations (defineMigration, CLI, deploy, drift). Load when: task involves data backfills, optional→required field hardening, field renames/removals, type narrowing, or kitcn migrate CLI commands. Skip for backward-compatible changes (new optional fields, new tables, code-level defaults).
references/features/create-plugins.md: canonical plugin authoring patterns (split package entries, token config, scaffold/lockfile/CLI manifest rules). Load when: creating or refactoring plugins.
references/features/ratelimit.md: ratelimit runtime accounting (shard budget dealing, check() vs limit(), snapshot conversion, read accuracy, failure modes). Load when: tuning shards, reading remaining quota, or debugging unexpected denials. Skip for plain ratelimit.middleware() wiring, which setup/server.md owns.
references/features/auth.md: full Better Auth core flow
references/features/auth-admin.md: admin plugin details
references/features/auth-organizations.md: org/multi-tenant plugin details
1---2name: kitcn3description: Use for Convex/kitcn setup and feature work: cRPC, ORM, auth, React.4---5# kitcn Core Skill (80% Path)6Use this file first for everyday feature delivery in an already configured kitcn app.7- If setup/bootstrap/env/auth wiring or project structure mirroring is missing, use `references/setup/index.md` (then the relevant setup file).8- If the task is advanced or niche, load only the specific feature reference listed at the end.9## Scope10In scope:11- Add or update schema tables, indexes, relations, and triggers.12- Implement cRPC procedures (`query`, `mutation`, `action`, `httpAction`) with runtime auth + rate limits.13- Implement feature UI with `useCRPC()` + TanStack Query.14- Add minimal high-value tests for auth, errors, and side effects.15Out of scope:16- Greenfield setup/install/env/bootstrap.17- Full plugin deep-dives (admin/organizations/polar).18- Internal package-level parity testing.19## Skill Contract201. Favor `ctx.orm` for app data access.212. Keep list/read paths bounded and index-aware.223. Use cRPC builders and middleware; avoid raw handler objects for new feature code.234. Use `CRPCError` for expected failures.245. 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).256. Keep auth/rate-limit checks server-side.267. **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.27## Shortcut Mode (tRPC + Drizzle Mental Model)28Default assumption:29- cRPC behavior is tRPC-like (builder chain + middleware + TanStack options).30- ORM behavior is Drizzle-like (schema, relations, `findMany/findFirst`, `insert/update/delete`).31Only remember these non-parity deltas:321. Procedure input root must be `z.object(...)` (no primitive root args).332. No `z.void()` outputs; omit `.output(...)` for no-value mutations.343. `.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.354. Stacked `.input(...)` calls merge input shapes.365. `.paginated({ limit, item })` must be before `.query()` and auto-adds `input.cursor` + `input.limit`, output `{ page, continueCursor, isDone }`.376. 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`.387. Auth metadata drives client behavior: `auth: "optional"` waits for auth load then runs, `auth: "required"` waits then skips when logged out.398. `ctx.orm` enforces constraints + RLS; `ctx.db` bypasses them.409. Non-paginated `findMany()` must be explicitly sized (`limit`, cursor mode, schema `defaultLimit`, or explicit `allowFullScan`).4110. Predicate `where` requires explicit `.withIndex(...)`; no implicit full scan fallback.4211. Cursor pagination uses the first `orderBy` field; index that field for stable paging.4312. `maxScan` applies to cursor mode only; `allowFullScan` is for non-cursor full-scan opt-in.4413. String operators / `columns` projection / many-relation subfilters can run post-fetch; bound result size early.4514. Search mode is relevance-ordered and does not support `orderBy`; vector mode has stricter limits (no cursor/offset/top-level where/order).4615. Update/delete without `where` throws unless `allowFullScan()`.4716. `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`.4817. cRPC React queries are real-time by default (`subscribe: true`); never use `queryClient.invalidateQueries` for these subscribed paths.4918. 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.5019. Better Auth Next.js shortcut is `convexBetterAuth(...)`; generic server-only shortcut is `createCallerFactory(...)`.5120. 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`.5221. **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`.5322. **`create<Module>Handler(ctx)`** is the default in queries/mutations: zero overhead, query/mutation ctx only, and no redundant validation or middleware.5423. **`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.5524. API types (`Api`, `ApiInputs`, `ApiOutputs`, `Select`, `Insert`, `TableName`) import from `@convex/api` — no manual `inferApiInputs<typeof api>`.5625. HTTP router must export as `httpRouter` (not `appRouter`) for codegen.5726. 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`.5827. `defineAuth(() => ({ ...options, triggers }))` replaces split `getAuthOptions` + `authTriggers`. Trigger callbacks are doc-first: `beforeCreate(data)`, `onCreate(doc)`, `onUpdate(newDoc, oldDoc)` — no `ctx` first param.5928. Internal auth functions at `internal.generated.*` (not `internal.auth.*`).6029. 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`.6130. 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.6231. 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.63## Directory Boundary64Use `references/setup/` when the task needs:651. Project/file structure setup → `setup/index.md` + `setup/server.md`662. Auth bootstrap → `setup/auth.md`673. Client/provider wiring → `setup/react.md`684. Framework-specific setup → `setup/next.md` or `setup/start.md`69For full template-level recreation: start with `setup/index.md`, then load relevant setup files, then load selected feature refs.70## First-Pass Feature Intake (Do This Before Edits)71Lock these decisions first:721. Auth level per endpoint: `public` / `optionalAuth` / `auth` / `private`.732. Data invariants: what must always be true after writes?743. Query shape: list, detail, relation-loaded, search, or stream composition.754. Pagination mode: offset, cursor, infinite.765. Side effects: trigger vs scheduled function vs inline mutation.776. UI consumption: client hook only, RSC prefetch, or server-only caller.787. Risk paths: unauthorized, forbidden, not found, conflicts, rate limit.79## Canonical File Targets80Typical feature touches:81- `convex/functions/schema.ts`82- `convex/functions/<feature>.ts`83- `convex/lib/crpc.ts` (only if middleware/procedure builder changes)84- `src/lib/convex/crpc.tsx` (only if cRPC context/meta wiring changes)85- `src/**` feature UI files86- `convex/functions/http.ts` or `convex/routers/**` for HTTP endpoints87- `convex/functions/crons.ts` or scheduled handlers if needed88## E2E Build Order (Default)891. Schema + indexes + relations.902. Trigger hooks for cross-row invariants (or explicit mutation-side sync if trigger path is unstable).913. Procedures with strict input/output + auth + rate limits.924. React hooks (query/mutation/infinite) using cRPC options.935. Optional: HTTP route(s), scheduling hooks.946. Tests for auth/error/trigger behavior.9596## Core Patterns9798### 1) Schema + Relations + Trigger99100```ts101import {102 convexTable,103 defineSchema,104 id,105 integer,106 index,107 text,108 timestamp,109} from "kitcn/orm";110111export const project = convexTable(112 "project",113 {114 name: text().notNull(),115 ownerId: id("user").notNull(),116 updatedAt: timestamp()117 .notNull()118 .defaultNow()119 .$onUpdateFn(() => new Date()),120 },121 (t) => [index("ownerId_updatedAt").on(t.ownerId, t.updatedAt)]122);123124export const task = convexTable(125 "task",126 {127 projectId: id("project").notNull(),128 title: text().notNull(),129 status: text().notNull().default("open"),130 updatedAt: timestamp()131 .notNull()132 .defaultNow()133 .$onUpdateFn(() => new Date()),134 },135 (t) => [index("projectId_updatedAt").on(t.projectId, t.updatedAt)]136);137138export default defineSchema({ project, task })139 .relations((r) => ({140 project: {141 tasks: r.many.task(),142 },143 task: {144 project: r.one.project({ from: r.task.projectId, to: r.project.id }),145 },146 }))147 .triggers({148 task: {149 change: async (change, ctx) => {150 const projectId = change.newDoc?.projectId ?? change.oldDoc?.projectId;151 if (!projectId) return;152 const open = await ctx.orm.query.task.findMany({153 where: { projectId, status: "open" },154 columns: { id: true },155 limit: 500,156 });157 await ctx.orm.update(project).set({ openTaskCount: open.length });158 },159 },160 });161```162163Schema rules that matter:1641651. Index fields that power filters/order/search.1662. `many()` relation paths need child FK indexes.1673. Trigger logic must be bounded and non-recursive.1684. Use table defaults for consistent write behavior.1695. Keep full ORM/query edge cases in `references/features/orm.md`.170171### 2) Procedure Builders + Middleware172173```ts174import { getSession } from "kitcn/auth";175import { CRPCError } from "kitcn/server";176import { initCRPC, type QueryCtx } from "../functions/generated/server";177178const c = initCRPC179 .meta<{180 auth?: "optional" | "required";181 role?: "admin";182 ratelimit?: string;183 }>()184 .create();185186function requireAuth<T>(user: T | null): T {187 if (!user) {188 throw new CRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });189 }190 return user;191}192async function getSessionUser(ctx: QueryCtx) {193 const session = await getSession(ctx);194 if (!session) return null;195 return await ctx.orm.query.user.findFirst({196 where: { id: { eq: session.userId } },197 });198}199200export const publicQuery = c.query.meta({ auth: "optional" });201export const authQuery = c.query202 .meta({ auth: "required" })203 .use(async ({ ctx, next }) => {204 const user = requireAuth(await getSessionUser(ctx));205 return next({ ctx: { ...ctx, user, userId: user.id } });206 });207export const authMutation = c.mutation208 .meta({ auth: "optional" })209 .use(async ({ ctx, next }) => {210 const user = await getSessionUser(ctx);211 return next({212 ctx: { ...ctx, user, userId: user?.id ?? null },213 });214 });215```216217Builder rules that matter:2182191. 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`.2202. `.meta(...)` is client-visible via generated API metadata. Never put secrets there.2213. 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.2224. Resolve session/user once in middleware. Do not re-fetch auth state in every procedure.223 Query/mutation middleware uses `getSession(ctx)` from `kitcn/auth`, which reads the session row224 directly. Keep `getAuth(ctx)` out of `convex/lib/crpc.ts`: it pulls the whole Better Auth225 definition and every auth plugin into the static import closure of every procedure module, and226 Convex has no dynamic `import()` to escape it. Import `getAuth` only in the modules that call227 `auth.api.*` — `convex/lib/crpc-action.ts`, HTTP routes, and organization/admin mutations.2285. Shared `c.middleware()` chains preserve mutation writer types on mutation procedures. If the middleware itself performs writes, type it as mutation-only with `c.middleware<MutationCtx>(...)`.2296. Keep deeper auth/runtime edge cases in `references/setup/server.md` and `references/features/auth*.md`.230231### 3) Query + Mutation Procedure Template232233```ts234import * as z from "zod";235import { eq } from "kitcn/orm";236import { CRPCError } from "kitcn/server";237import { authMutation, authQuery } from "../lib/crpc";238import { project } from "./schema";239240export const listProjects = authQuery241 .paginated({ limit: z.number().min(1).max(50).default(20), item: project })242 .query(async ({ ctx, input }) =>243 ctx.orm.query.project.findMany({244 where: { ownerId: ctx.userId },245 orderBy: { updatedAt: "desc" },246 cursor: input.cursor,247 limit: input.limit,248 })249 );250251export const renameProject = authMutation252 .input(z.object({ id: z.string(), name: z.string().min(1).max(120) }))253 .mutation(async ({ ctx, input }) => {254 const current = await ctx.orm.query.project.findFirst({255 where: { id: input.id, ownerId: ctx.userId },256 columns: { id: true },257 });258 if (!current) {259 throw new CRPCError({ code: "NOT_FOUND", message: "Project not found" });260 }261 await ctx.orm262 .update(project)263 .set({ name: input.name })264 .where(eq(project.id, current.id));265 return null;266 });267```268269Procedure rules that matter:2702711. Root input must be `z.object(...)`.2722. Use strict `.input(...)`; add `.output(...)` only when needed.2733. Omit `.output(...)` for no-value mutations.2744. Use the default mutation rate limit; add `.meta({ ratelimit: ... })` only for named bucket overrides.2755. Throw `CRPCError` for expected outcomes.2766. Bound every list with `limit`, cursor, or `.paginated(...)`.2777. Move advanced query-builder shapes to `references/features/orm.md`.278279### 3b) Inter-Procedure Composition280281Use:2822831. `create<Module>Handler(ctx)` in queries/mutations.2842. `create<Module>Caller(ctx)` in actions/HTTP routes.2853. `caller.actions.*` for action procedures.2864. `caller.schedule.*` for scheduled procedures.2875. Never `ctx.runQuery` / `ctx.runMutation` / `ctx.runAction` for module procedures.288289### 4) Query Modes (Use The Right One)2902911. Default to object `where`.2922. Use callback `where` only when composition reads better than object form.2933. Predicate/filter callbacks require `.withIndex(...)` first plus explicit `limit`/`maxScan`.2944. Full-text search uses `search: { index, query, filters }` and does not support `orderBy`.2955. Cursor paging is only stable when the `orderBy` field is indexed.2966. Advanced modes (`pageByKey`, vector search, pipelines, aggregate indexes) live in `references/features/orm.md`.297298### 5) Mutation Patterns (Most Used)2993001. Use `.returning(...)` on inserts when caller needs created ids.3012. Every update/delete path gets an explicit `where(...)`.3023. Clear optional columns with `unsetToken`.3034. Async mutation execution is the default; use `.execute({ mode: "sync" })` only when atomic all-at-once behavior is required.3045. Prefer set-based deletes/updates. Add chunking only for external side effects or bounded cleanups.3056. Upsert, conflict handling, mutation batching, and schema extension edge cases live in `references/features/orm.md`.306307### 6) Error Model308309Use this map consistently:3103111. `BAD_REQUEST`: invalid input or business precondition.3122. `UNAUTHORIZED`: no session.3133. `FORBIDDEN`: session exists, permission missing.3144. `NOT_FOUND`: missing or inaccessible resource.3155. `CONFLICT`: duplicate or conflicting write.3166. `TOO_MANY_REQUESTS`: rate limit.3177. `INTERNAL_SERVER_ERROR`: unexpected failures only. cRPC also raises it for a318 failed `.output(...)` parse, with message `Output validation failed` and319 sanitized structural Zod issues in `error.data.ZodError`. Custom issue320 messages and fields stay server-side because they can contain handler output.3218. Add small custom `data` payloads on `CRPCError` when the client needs322 domain metadata like conflicting ids. Read them on the client from323 `error.data`.324325Required tests:3263271. unauthenticated rejection3282. permission rejection when relevant3293. missing resource path3304. conflict path when relevant3315. rate-limited write path when relevant332333### 7) React Query Integration334335Preconditions (must be true before writing/using `useCRPC()` code paths):3363371. Generated imports exist (`@convex/api`) from setup bootstrap.3382. Provider chain is mounted (`CRPCProvider` inside QueryClient + Convex provider flow).3393. If bootstrap/provider prerequisites are missing, stop feature work and finish `references/setup/` first.3404. Backend state is project-local in `.convex/`, not `~/.convex`.341342`useCRPC()` pattern: `const crpc = useCRPC(); const projects = useQuery(crpc.project.listProjects.queryOptions({ cursor: null, limit: 20 })); const createProject = useMutation(crpc.project.createProject.mutationOptions());`343344Key client defaults/deltas:3453461. Queries are real-time by default (`subscribe: true`).3472. Never use `queryClient.invalidateQueries` for subscribed cRPC query paths.3483. Use `{ subscribe: false }` only for one-time fetches; refresh those with explicit `refetch`/`fetchQuery`.3494. Use `skipUnauth: true` to avoid unauthorized fetch churn.3505. For pagination, use `useInfiniteQuery` from `kitcn/react`.3516. Prefer typed `queryKey(...)` helpers for cache read/write/fetch ops instead of manual keys.3527. For kitcn auth flows, prefer `createAuthMutations(...)` wrappers (not raw auth client calls) to avoid logout race errors. Raw Convex preset keeps the plain auth client path.3538. For mutation toasts, prefer `error.data?.message` over `error.message`; `data.message` is the clean `CRPCError` payload.3549. Prefer one global `QueryClient` mutation `onError` toast with `mutation.meta.errorMessage` / `skipErrorToast` rather than copy-pasting `onError` in every component.35510. Full client/RSC depth lives in `references/features/react.md`.356357### 8) RSC Patterns (Next.js)358359Choose one per use case:3603611. `prefetch(...)` (preferred): non-blocking, hydrated, client owns data.3622. `caller.*`: blocking server-only logic (redirects/auth checks), not hydrated.3633. `preloadQuery(...)`: blocking + hydrated when server needs data immediately.364365Do not render `preloadQuery` result on server and again on client for the same data path.3663671. `HydrateClient` must wrap all client components that consume prefetched queries.3682. Next.js-specific setup and deeper hydration tradeoffs live in `references/setup/next.md` and `references/features/react.md`.369370### 9) HTTP Route Pattern (When Feature Needs REST/Webhooks)371372```ts373import { createTaskCaller } from "../functions/generated/task.runtime";374375export const createTaskRoute = authRoute376 .post("/api/projects/:projectId/tasks")377 .params(z.object({ projectId: z.string() }))378 .input(z.object({ title: z.string().min(1) }))379 .output(z.object({ id: z.string() }))380 .mutation(async ({ ctx, params, input }) => {381 const caller = createTaskCaller(ctx);382 const id = await caller.createFromHttp({383 projectId: params.projectId,384 title: input.title,385 userId: ctx.userId,386 });387 return { id };388 });389```390391HTTP-specific rules:3923931. Use `z.coerce.*` for search params.3942. Keep auth and permission checks in middleware/procedure.3953. Apply rate limits to public/heavy endpoints.3964. Validate webhook signatures before any side effects.3975. Use `publicRoute` / `authRoute` / `optionalAuthRoute` builders from `convex/lib/crpc.ts`.3986. Compose endpoints with `router(...)` for feature-level HTTP grouping.3997. Client calls must pass path/query args as `{ params, searchParams }`; query values are strings.4008. Webhooks, streaming, and Hono-specific patterns live in `references/features/http.md`.401402### 10) Scheduling Pattern (If Needed)403404Example: `const caller = createTaskCaller(ctx); await caller.schedule.now.sendTaskCreated({ taskId: created.id, userId: ctx.userId }); await caller.schedule.at(input.sendAt).sendReminder({ taskId: input.taskId, userId: ctx.userId });`405406Scheduling rules:4074081. Auth context is not propagated; pass user/org IDs explicitly.4092. Mutation scheduling is atomic with the mutation transaction.4103. Store returned job IDs when cancellation is required.4114. Scheduling inside actions is not atomic with action failure.4125. Cron schedules run in UTC.4136. Use `ctx.scheduler.*` directly only when you must schedule non-procedure `internal.*` functions.4147. Cron expressions and operational details live in `references/features/scheduling.md`.415416### 11) Testing Baseline (High Signal)417418Minimum feature test set:4194201. happy path query/mutation4212. unauthenticated rejection (`UNAUTHORIZED`)4223. permission/ownership rejection (`FORBIDDEN` where relevant)4234. missing resource (`NOT_FOUND`)4245. trigger side effect assertion4256. scheduler assertion if feature schedules work4267. not-found checks should use real IDs or non-ID lookup keys (slug/name/email), not synthetic IDs4278. Full testing recipes live in `references/features/testing.md`.4289. If Convex bootstrap blocks integration tests, extract pure guards/helpers and keep one smoke integration test once bootstrap works.429430## Performance + Safety Checklist431432Before calling a feature done:4334341. Every list query is bounded (`limit`/cursor).4352. Filters/order align with indexes.4363. Expensive post-fetch logic uses pre-narrowed index path.4374. Mutations use targeted `where` and avoid accidental full scans.4385. Trigger logic is bounded, idempotent, and avoids ping-pong loops.4396. Error codes are explicit and intentional.4407. User-facing writes have rate-limit metadata.4418. Tests cover auth + not-found + side effects.4429. `ctx.db` is not used on paths that rely on ORM constraints/RLS.44310. Paginated endpoints use `.paginated(...)` + ORM cursor flow (not ad-hoc wrappers).44411. For any predicate/full-scan-like path, `.withIndex(...)` + bound (`limit`/`maxScan`) is explicit.44512. NEVER use `@ts-nocheck`, no global lint-rule downgrades, no unresolved lint warnings in touched files.446447## Common Mistakes (And Fixes)448449| Mistake | Correct pattern |450| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |451| Raw Convex handler for new feature procedures | cRPC builders (`publicQuery`, `authMutation`, etc.) |452| Write-time side effects duplicated across mutations | Schema trigger, or one centralized mutation-side sync helper when trigger path is unsafe |453| Missing bounds on list/search | Add `limit` + cursor/pagination |454| `orderBy` written as array objects | Use object form: `orderBy: { updatedAt: "desc" }` |455| Using `ctx.db` for policy-sensitive reads | Use `ctx.orm` (RLS/constraints path) |456| Throwing generic `Error` for expected outcomes | Throw `CRPCError` with explicit code |457| Infinite list with TanStack native hook directly | Use `useInfiniteQuery` from `kitcn/react` |458| Primitive root input (`z.string()`) | Use root `z.object(...)` input schema |459| Returning nothing with `z.void()` | Omit explicit output |460| Returning a possibly-missing lookup under `.output(...nullable())` | Coalesce it: `?? null`. `.output(...)` substitutes nothing for `undefined` |461| Manual pagination wrappers for infinite endpoints | Use `.paginated({ limit, item })` |462| Synthetic Convex IDs in tests (`"missing-id"`) | Use inserted IDs or semantic lookup keys |463| Aggregates disabled but helper/config still present | Remove aggregate helper + `defineTriggers` handlers + app config together |464| Putting secrets in `.meta(...)` | Keep metadata non-sensitive (client-visible) |465| Using `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction` directly | Use `create<Module>Handler(ctx)` in queries/mutations, `create<Module>Caller(ctx)` in actions/HTTP with `caller.actions.*` / `caller.schedule.*` (from `generated/<module>.runtime`) |466| Using `createCaller` in query/mutation context | Use `create<Module>Handler(ctx)` — zero overhead, bypasses redundant validation |467| Adding `// @ts-nocheck` to unblock compile | NEVER do this; fix the underlying types using canonical patterns in `references/setup/` |468| Relaxing lint rules to pass checks | Keep baseline lint config; fix code-level warnings/errors instead |469470## Reference Escalation Map (Load Only If Needed)471472**Setup (once per project):**473474- `references/setup/index.md`: bootstrap, env, decision intake, gates, checklist, troubleshooting475- `references/setup/server.md`: core backend (schema, ORM, cRPC) + optional module gates476- `references/setup/auth.md`: auth core bootstrap + plugin setup477- `references/setup/react.md`: client core (QueryClient, provider, cRPC context)478- `references/setup/next.md`: Next.js App Router setup479- `references/setup/start.md`: TanStack Start setup480- `references/setup/doc-guidelines.md`: skill/docs sync contract481482**Features (per session, self-contained):**483484- `references/features/orm.md`: full ORM API, constraints, RLS, advanced mutations, filtering/search/composition/pagination485- `references/features/react.md`: full client, RSC, hydration, error handling matrix486- `references/features/http.md`: typed REST routes, webhooks, streaming487- `references/features/scheduling.md`: cron + delayed job patterns488- `references/features/testing.md`: deeper testing scenarios489- `references/features/aggregates.md`: aggregate component patterns490- `references/features/migrations.md`: built-in online data migrations (defineMigration, CLI, deploy, drift). Load when: task involves data backfills, optional→required field hardening, field renames/removals, type narrowing, or `kitcn migrate` CLI commands. Skip for backward-compatible changes (new optional fields, new tables, code-level defaults).491- `references/features/create-plugins.md`: canonical plugin authoring patterns (split package entries, token config, scaffold/lockfile/CLI manifest rules). Load when: creating or refactoring plugins.492- `references/features/ratelimit.md`: ratelimit runtime accounting (shard budget dealing, `check()` vs `limit()`, snapshot conversion, read accuracy, failure modes). Load when: tuning `shards`, reading remaining quota, or debugging unexpected denials. Skip for plain `ratelimit.middleware()` wiring, which `setup/server.md` owns.493- `references/features/auth.md`: full Better Auth core flow494- `references/features/auth-admin.md`: admin plugin details495- `references/features/auth-organizations.md`: org/multi-tenant plugin details