mit einem Klick
kitcn
// ALWAYS use this skill when working with convex or kitcn. Covers both setup and e2e feature paths using cRPC + ORM + auth + React.
// ALWAYS use this skill when working with convex or kitcn. Covers both setup and e2e feature paths using cRPC + ORM + auth + React.
Codex autoreview/code review closeout: local dirty changes, PR branch vs main, parallel tests.
Open a concise GitHub follow-up for reusable browser-use limitations. Use when browser automation is blocked by a likely tool-side issue that is worth fixing separately, especially for clicks, dropdowns, file inputs, focus traps, or other repeatable agent/browser failures.
Fallback browser automation with persistent Chrome state. Use only when Browser Use is unavailable or blocked.
Work heavyweight framework or library tasks with planning-first research, selective deep analysis, and rigorous handoff
Work a task end-to-end with lean context gathering, implementation, and verification
Read every doc in www and packages/kitcn/skills/kitcn, sync to active changeset(s), and track with checkmarks.
| name | kitcn |
| description | ALWAYS use this skill when working with convex or kitcn. Covers both setup and e2e feature paths using 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"] |
| metadata | {"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"]} |
Use this file first for everyday feature delivery in an already configured kitcn app.
references/setup/index.md (then the relevant setup file).In scope:
query, mutation, action, httpAction) with runtime auth + rate limits.useCRPC() + TanStack Query.ctx.orm for app data access.CRPCError for expected failures.create<Module>Handler(ctx) in queries/mutations (zero overhead) unless validation is relevant, create<Module>Caller(ctx) in actions/HTTP routes. In action context use caller.actions.* for action procedures and caller.schedule.* for scheduling. Import from ./generated/<module>.runtime. Never call ctx.runQuery/ctx.runMutation/ctx.runAction directly for module procedures.Default assumption:
findMany/findFirst, insert/update/delete).
Only remember these non-parity deltas:z.object(...) (no primitive root args).z.void() outputs; omit .output(...) for no-value mutations..input(...) calls merge input shapes..paginated({ limit, item }) must be before .query() and auto-adds input.cursor + input.limit, output { page, continueCursor, isDone }.@convex/api leaves (api.namespace.fn.meta) so never put secrets in .meta(...); chaining .meta(...) is shallow merge and supports defaultMeta.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.findMany() must be explicitly sized (limit, cursor mode, schema defaultLimit, or explicit allowFullScan).where requires explicit .withIndex(...); no implicit full scan fallback.orderBy field; index that field for stable paging.maxScan applies to cursor mode only; allowFullScan is for non-cursor full-scan opt-in.columns projection / many-relation subfilters can run post-fetch; bound result size early.orderBy; vector mode has stricter limits (no cursor/offset/top-level where/order).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.subscribe: true); never use queryClient.invalidateQueries for these subscribed paths.prefetch hydrates client, caller is server-only and not hydrated, preloadQuery hydrates but can cause stale split ownership if also rendered client-side.convexBetterAuth(...); generic server-only shortcut is createCallerFactory(...).createAuthMutations(authClient) wrappers so logout unsubscribes auth queries before sign out. Raw Convex preset keeps a smaller plain authClient.ctx.runQuery/ctx.runMutation/ctx.runAction directly for module-to-module calls. Use create<Module>Handler(ctx) or create<Module>Caller(ctx) from convex/functions/generated/<module>.runtime instead.create<Module>Handler(ctx) — default choice for queries/mutations. Bypasses input validation, middleware, output validation → zero overhead. Query/mutation ctx only. Import from ./generated/<module>.runtime.create<Module>Caller(ctx) — use in actions and HTTP routes (where handler is unavailable). Goes through validation + middleware. Root caller exposes query+mutation procedures. In ActionCtx, action procedures are under caller.actions.*; scheduling is under caller.schedule.now|after|at with caller.schedule.cancel(id). Use requireActionCtx(ctx) only when the callback truly runs in ActionCtx and you need caller.actions.*. If the callback can run from MutationCtx | ActionCtx or generic scheduler-capable context, keep the seam honest: use requireSchedulerCtx(ctx) and caller.schedule.* instead of pretending the path is action-only. Import from ./generated/<module>.runtime. Each caller/handler eagerly loads every procedure in its module (no lazy loading) — split large modules to keep bundles lean.Api, ApiInputs, ApiOutputs, Select, Insert, TableName) import from @convex/api — no manual inferApiInputs<typeof api>.httpRouter (not appRouter) for codegen.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.generated.* (not internal.auth.*).execute({ batchSize, delayMs }). Opt into sync: execute({ mode: 'sync' }) or defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } }). Relevant defaults: mutationBatchSize, mutationLeafBatchSize, mutationMaxRows, mutationScheduleCallCap.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.Use references/setup/ when the task needs:
setup/index.md + setup/server.mdsetup/auth.mdsetup/react.mdsetup/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.Lock these decisions first:
public / optionalAuth / auth / private.Typical feature touches:
convex/functions/schema.tsconvex/functions/<feature>.tsconvex/lib/crpc.ts (only if middleware/procedure builder changes)src/lib/convex/crpc.tsx (only if cRPC context/meta wiring changes)src/** feature UI filesconvex/functions/http.ts or convex/routers/** for HTTP endpointsconvex/functions/crons.ts or scheduled handlers if neededimport {
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:
many() relation paths need child FK indexes.references/features/orm.md.import { getHeaders } from "kitcn/auth";
import { CRPCError } from "kitcn/server";
import { getAuth } from "../functions/generated/auth";
import { initCRPC } 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;
}
export const publicQuery = c.query.meta({ auth: "optional" });
export const authQuery = c.query
.meta({ auth: "required" })
.use(async ({ ctx, next }) => {
const auth = getAuth(ctx);
const session = await auth.api.getSession({
headers: await getHeaders(ctx),
});
const user = requireAuth(session?.user ?? null);
return next({ ctx: { ...ctx, user, userId: user.id } });
});
export const authMutation = c.mutation
.meta({ auth: "optional" })
.use(async ({ ctx, next }) => {
const auth = getAuth(ctx);
const session = await auth.api.getSession({
headers: await getHeaders(ctx),
});
return next({
ctx: {
...ctx,
user: session?.user ?? null,
userId: session?.user?.id ?? null,
},
});
});
Builder rules that matter:
public, optional, auth, and private procedure families once in convex/lib/crpc.ts..meta(...) is client-visible via generated API metadata. Never put secrets there.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.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>(...).references/setup/server.md and references/features/auth*.md.import { 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:
z.object(...)..input(...); add .output(...) only when needed..output(...) for no-value mutations..meta({ ratelimit: ... }) only for named bucket overrides.CRPCError for expected outcomes.limit, cursor, or .paginated(...).references/features/orm.md.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.ctx.runQuery / ctx.runMutation / ctx.runAction for module procedures.where.where only when composition reads better than object form..withIndex(...) first plus explicit limit/maxScan.search: { index, query, filters } and does not support orderBy.orderBy field is indexed.pageByKey, vector search, pipelines, aggregate indexes) live in references/features/orm.md..returning(...) on inserts when caller needs created ids.where(...).unsetToken..execute({ mode: "sync" }) only when atomic all-at-once behavior is required.references/features/orm.md.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.data payloads on CRPCError when the client needs
domain metadata like conflicting ids. Read them on the client from
error.data.Required tests:
Preconditions (must be true before writing/using useCRPC() code paths):
@convex/api) from setup bootstrap.CRPCProvider inside QueryClient + Convex provider flow).references/setup/ first..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:
subscribe: true).queryClient.invalidateQueries for subscribed cRPC query paths.{ subscribe: false } only for one-time fetches; refresh those with explicit refetch/fetchQuery.skipUnauth: true to avoid unauthorized fetch churn.useInfiniteQuery from kitcn/react.queryKey(...) helpers for cache read/write/fetch ops instead of manual keys.createAuthMutations(...) wrappers (not raw auth client calls) to avoid logout race errors. Raw Convex preset keeps the plain auth client path.error.data?.message over error.message; data.message is the clean CRPCError payload.QueryClient mutation onError toast with mutation.meta.errorMessage / skipErrorToast rather than copy-pasting onError in every component.references/features/react.md.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.references/setup/next.md and references/features/react.md.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:
z.coerce.* for search params.publicRoute / authRoute / optionalAuthRoute builders from convex/lib/crpc.ts.router(...) for feature-level HTTP grouping.{ params, searchParams }; query values are strings.references/features/http.md.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:
ctx.scheduler.* directly only when you must schedule non-procedure internal.* functions.references/features/scheduling.md.Minimum feature test set:
UNAUTHORIZED)FORBIDDEN where relevant)NOT_FOUND)references/features/testing.md.Before calling a feature done:
limit/cursor).where and avoid accidental full scans.ctx.db is not used on paths that rely on ORM constraints/RLS..paginated(...) + ORM cursor flow (not ad-hoc wrappers)..withIndex(...) + bound (limit/maxScan) is explicit.@ts-nocheck, no global lint-rule downgrades, no unresolved lint warnings in touched files.| 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 |
| 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 |
Setup (once per project):
references/setup/index.md: bootstrap, env, decision intake, gates, checklist, troubleshootingreferences/setup/server.md: core backend (schema, ORM, cRPC) + optional module gatesreferences/setup/auth.md: auth core bootstrap + plugin setupreferences/setup/react.md: client core (QueryClient, provider, cRPC context)references/setup/next.md: Next.js App Router setupreferences/setup/start.md: TanStack Start setupreferences/setup/doc-guidelines.md: skill/docs sync contractFeatures (per session, self-contained):
references/features/orm.md: full ORM API, constraints, RLS, advanced mutations, filtering/search/composition/paginationreferences/features/react.md: full client, RSC, hydration, error handling matrixreferences/features/http.md: typed REST routes, webhooks, streamingreferences/features/scheduling.md: cron + delayed job patternsreferences/features/testing.md: deeper testing scenariosreferences/features/aggregates.md: aggregate component patternsreferences/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/auth.md: full Better Auth core flowreferences/features/auth-admin.md: admin plugin detailsreferences/features/auth-organizations.md: org/multi-tenant plugin details