Skip to main content

kitcn

Use for Convex/kitcn setup and feature work: cRPC, ORM, auth, React.

Source facts

Repository
udecode/kitcn
Last source activity
October 1, 2026 at 09:26
Detected SKILL.md language
English
Stars
450
Forks
34

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
23 files

Showing SKILL.md

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