with one click
kitcn
Use for Convex/kitcn setup and feature work: cRPC, ORM, auth, React.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Use for Convex/kitcn setup and feature work: cRPC, ORM, auth, React.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
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.
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
Sync `zbeyens/convex-better-auth` with upstream, then sync kitcn against upstream `convex-better-auth` changes. Use when asked to run `sync-convex-auth`, compare the fork with upstream, fast-forward or PR the fork update when safe, audit commits the fork was behind on, classify relevance to kitcn auth integration, and delegate one implementation PR through `task`.
Audit newer Convex npm releases against kitcn. Use when checking whether a newer `convex` version unlocks kitcn improvements, compatibility work, CLI/agent workflows, or cleanup of local Convex hacks. Reads `https://ship.convex.dev/`, the upstream Convex changelog, and GitHub diffs before delegating an implementation PR through `task`.
Read every doc in www and packages/kitcn/skills/kitcn, sync to active changeset(s), and track with checkmarks.
| 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"] |
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, 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.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 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, 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