kitcn
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesekitcn Core Skill (80% Path)
kitcn核心技能(80%通用流程)
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 (then the relevant setup file).
references/setup/index.md - If the task is advanced or niche, load only the specific feature reference listed at the end.
在已配置完成的kitcn应用中,日常功能开发请优先使用本文件。
- 如果缺少搭建/初始化/环境/鉴权配置或项目结构镜像,请使用(之后再使用相关的搭建文件)。
references/setup/index.md - 如果任务属于高级或小众场景,仅加载末尾列出的特定功能参考文档。
Scope
适用范围
In scope:
- Add or update schema tables, indexes, relations, and triggers.
- Implement cRPC procedures (,
query,mutation,action) with runtime auth + rate limits.httpAction - Implement feature UI with + TanStack Query.
useCRPC() - 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.
适用场景:
- 添加或更新schema表、索引、关联关系和触发器。
- 实现带有运行时鉴权+速率限制的cRPC过程(、
query、mutation、action)。httpAction - 使用+ TanStack Query实现功能UI。
useCRPC() - 为鉴权、错误处理和副作用添加高价值的极简测试。 不适用场景:
- 全新项目的搭建/安装/环境/初始化。
- 完整插件深度解析(admin/organizations/polar)。
- 内部包级别的一致性测试。
Skill Contract
技能约定
- Favor for app data access.
ctx.orm - Keep list/read paths bounded and index-aware.
- Use cRPC builders and middleware; avoid raw handler objects for new feature code.
- Use for expected failures.
CRPCError - 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: in queries/mutations (zero overhead) unless validation is relevant,
create<Module>Handler(ctx)in actions/HTTP routes. In action context usecreate<Module>Caller(ctx)for action procedures andcaller.actions.*for scheduling. Import fromcaller.schedule.*. Never call./generated/<module>.runtime/ctx.runQuery/ctx.runMutationdirectly for module procedures.ctx.runAction
- 优先使用进行应用数据访问。
ctx.orm - 列表/读取路径需限制范围并考虑索引。
- 使用cRPC构建器和中间件;新功能代码避免使用原始处理器对象。
- 预期失败场景使用抛出错误。
CRPCError - 优先使用schema触发器维护跨行约束,但如果触发器执行不稳定(例如初始化/种子数据挂起或递归写入路径),则将约束维护逻辑迁移到显式的mutation辅助函数中。
- 鉴权/速率限制检查需放在服务端。
- 跨过程调用:在queries/mutations中使用(零开销),除非需要验证;在actions/HTTP路由中使用
create<Module>Handler(ctx)。在action上下文里,使用create<Module>Caller(ctx)调用action过程,使用caller.actions.*进行任务调度。从caller.schedule.*导入。绝不要直接调用./generated/<module>.runtime/ctx.runQuery/ctx.runMutation来执行模块过程。ctx.runAction
Shortcut Mode (tRPC + Drizzle Mental Model)
快捷模式(tRPC + Drizzle思维模型)
Default assumption:
- cRPC behavior is tRPC-like (builder chain + middleware + TanStack options).
- ORM behavior is Drizzle-like (schema, relations, ,
findMany/findFirst). Only remember these non-parity deltas:insert/update/delete
- Procedure input root must be (no primitive root args).
z.object(...) - No outputs; omit
z.void()for no-value mutations..output(...) - Stacked calls merge input shapes.
.input(...) - must be before
.paginated({ limit, item })and auto-adds.query()+input.cursor, outputinput.limit.{ page, continueCursor, isDone } - Metadata is codegen’d onto leaves (
@convex/api) so never put secrets inapi.namespace.fn.meta; chaining.meta(...)is shallow merge and supports.meta(...).defaultMeta - Auth metadata drives client behavior: waits for auth load then runs,
auth: "optional"waits then skips when logged out.auth: "required" - enforces constraints + RLS;
ctx.ormbypasses them.ctx.db - Non-paginated must be explicitly sized (
findMany(), cursor mode, schemalimit, or explicitdefaultLimit).allowFullScan - Predicate requires explicit
where; no implicit full scan fallback..withIndex(...) - Cursor pagination uses the first field; index that field for stable paging.
orderBy - applies to cursor mode only;
maxScanis for non-cursor full-scan opt-in.allowFullScan - String operators / projection / many-relation subfilters can run post-fetch; bound result size early.
columns - Search mode is relevance-ordered and does not support ; vector mode has stricter limits (no cursor/offset/top-level where/order).
orderBy - Update/delete without throws unless
where.allowFullScan() - ,
count(), andaggregate()require a matchinggroupBy(). UseaggregateIndexinstead of multiplegroupBy({ by, _count, _sum })calls or.count()+ manual JS grouping. EveryfindManyfield must be finite-constrained (by/eq/in) inisNull. Seewhere.references/features/aggregates.md - cRPC React queries are real-time by default (); never use
subscribe: truefor these subscribed paths.queryClient.invalidateQueries - In RSC, hydrates client,
prefetchis server-only and not hydrated,callerhydrates but can cause stale split ownership if also rendered client-side.preloadQuery - Better Auth Next.js shortcut is ; generic server-only shortcut is
convexBetterAuth(...).createCallerFactory(...) - On the kitcn auth client path, use wrappers so logout unsubscribes auth queries before sign out. Raw Convex preset keeps a smaller plain
createAuthMutations(authClient).authClient - NEVER use /
ctx.runQuery/ctx.runMutationdirectly for module-to-module calls. Usectx.runActionorcreate<Module>Handler(ctx)fromcreate<Module>Caller(ctx)instead.convex/functions/generated/<module>.runtime - — default choice for queries/mutations. Bypasses input validation, middleware, output validation → zero overhead. Query/mutation ctx only. Import from
create<Module>Handler(ctx)../generated/<module>.runtime - — use in actions and HTTP routes (where handler is unavailable). Goes through validation + middleware. Root caller exposes query+mutation procedures. In
create<Module>Caller(ctx), action procedures are underActionCtx; scheduling is undercaller.actions.*withcaller.schedule.now|after|at. Usecaller.schedule.cancel(id)only when the callback truly runs inrequireActionCtx(ctx)and you needActionCtx. If the callback can run fromcaller.actions.*or generic scheduler-capable context, keep the seam honest: useMutationCtx | ActionCtxandrequireSchedulerCtx(ctx)instead of pretending the path is action-only. Import fromcaller.schedule.*. Each caller/handler eagerly loads every procedure in its module (no lazy loading) — split large modules to keep bundles lean../generated/<module>.runtime - API types (,
Api,ApiInputs,ApiOutputs,Select,Insert) import fromTableName— no manual@convex/api.inferApiInputs<typeof api> - HTTP router must export as (not
httpRouter) for codegen.appRouter - Server wiring imports come from directory:
convex/functions/generated/,getAuthfromdefineAuth;generated/auth,initCRPC,QueryCtx,MutationCtxfromOrmCtx;generated/server,create<Module>Callerfromcreate<Module>Handler. No manualgenerated/<module>.runtime.convex/lib/orm.ts - replaces split
defineAuth(() => ({ ...options, triggers }))+getAuthOptions. Trigger callbacks are doc-first:authTriggers,beforeCreate(data),onCreate(doc)— noonUpdate(newDoc, oldDoc)first param.ctx - Internal auth functions at (not
internal.generated.*).internal.auth.* - Async mutation batching is the default (codegen wires it). Customize per call: . Opt into sync:
execute({ batchSize, delayMs })orexecute({ mode: 'sync' }). Relevant defaults:defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } }),mutationBatchSize,mutationLeafBatchSize,mutationMaxRows.mutationScheduleCallCap - Polymorphic unions are schema-first: use in
actionType: discriminator({ variants, as? }). Query config does not include aconvexTable(...)option. Writes stay flat; reads synthesize nestedpolymorphic(or custom alias). Usedetailsto auto-load allwithVariants: truerelations on discriminator tables.one() - 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.
默认假设:
- cRPC行为类似tRPC(构建器链 + 中间件 + TanStack配置)。
- ORM行为类似Drizzle(schema、关联关系、、
findMany/findFirst)。 只需记住以下差异点:insert/update/delete
- 过程的根输入必须是(不支持原始类型根参数)。
z.object(...) - 不支持输出;无返回值的mutations需省略
z.void()。.output(...) - 多次调用会合并输入结构。
.input(...) - 必须放在
.paginated({ limit, item })之前,会自动添加.query()+input.cursor,输出input.limit。{ page, continueCursor, isDone } - 元数据会自动生成到的叶子节点(
@convex/api),因此绝不要在api.namespace.fn.meta中存放敏感信息;链式调用.meta(...)会进行浅合并,并且支持.meta(...)。defaultMeta - 鉴权元数据控制客户端行为:会等待鉴权加载完成后执行,
auth: "optional"会等待鉴权完成,若未登录则跳过执行。auth: "required" - 会强制约束+RLS(行级安全);
ctx.orm会绕过这些限制。ctx.db - 非分页的必须显式限制大小(
findMany()、游标模式、schema的limit,或显式声明defaultLimit)。allowFullScan - 断言式必须显式调用
where;不支持隐式全表扫描回退。.withIndex(...) - 游标分页使用第一个字段;需为该字段创建索引以保证分页稳定性。
orderBy - 仅适用于游标模式;
maxScan用于非游标模式下的全表扫描授权。allowFullScan - 字符串操作符 / 投影 / 多关联子过滤可在获取数据后执行;需尽早限制结果集大小。
columns - 搜索模式按相关性排序,不支持;向量模式有更严格的限制(不支持游标/偏移/顶级where/排序)。
orderBy - 不带的Update/Delete操作会抛出错误,除非声明
where。allowFullScan() - 、
count()和aggregate()需要匹配的groupBy()。使用aggregateIndex替代多次groupBy({ by, _count, _sum })调用或.count()+ 手动JS分组。findMany中的每个where字段必须是有限约束的(by/eq/in)。详见isNull。references/features/aggregates.md - cRPC React查询默认是实时的();对于这些已订阅的路径,绝不要使用
subscribe: true。queryClient.invalidateQueries - 在RSC(React Server Components)中,会为客户端注入数据,
prefetch仅服务端可用且不会注入数据,caller会注入数据,但如果同时在客户端渲染,可能会导致数据所有权不一致的过期问题。preloadQuery - Better Auth Next.js快捷方式是;通用服务端快捷方式是
convexBetterAuth(...)。createCallerFactory(...) - 在kitcn鉴权客户端路径中,使用包装器,以便登出时先取消订阅鉴权查询再执行退出操作。原生Convex预设使用更轻量的普通
createAuthMutations(authClient)。authClient - 绝不要直接使用/
ctx.runQuery/ctx.runMutation进行模块间调用。请使用ctx.runAction中的convex/functions/generated/<module>.runtime或create<Module>Handler(ctx)替代。create<Module>Caller(ctx) - —— queries/mutations的默认选择。绕过输入验证、中间件、输出验证 → 零开销。仅适用于Query/Mutation上下文。从
create<Module>Handler(ctx)导入。./generated/<module>.runtime - —— 在actions和HTTP路由中使用(此时handler不可用)。会经过验证+中间件。根caller暴露query+mutation过程。在
create<Module>Caller(ctx)中,action过程位于ActionCtx;调度功能位于caller.actions.*,可使用caller.schedule.now|after|at取消任务。仅当回调确实在caller.schedule.cancel(id)中运行且需要ActionCtx时,才使用caller.actions.*。如果回调可在requireActionCtx(ctx)或通用支持调度的上下文中运行,请保持逻辑清晰:使用MutationCtx | ActionCtx和requireSchedulerCtx(ctx),而非强行假设路径仅为action专用。从caller.schedule.*导入。每个caller/handler会预加载模块中的所有过程(无懒加载)—— 拆分大型模块以减小包体积。./generated/<module>.runtime - API类型(、
Api、ApiInputs、ApiOutputs、Select、Insert)从TableName导入 —— 无需手动使用@convex/api。inferApiInputs<typeof api> - HTTP路由必须导出为(而非
httpRouter)以支持代码生成。appRouter - 服务端配置导入来自目录:
convex/functions/generated/、getAuth来自defineAuth;generated/auth、initCRPC、QueryCtx、MutationCtx来自OrmCtx;generated/server、create<Module>Caller来自create<Module>Handler。不要手动导入generated/<module>.runtime。convex/lib/orm.ts - 替代拆分的
defineAuth(() => ({ ...options, triggers }))+getAuthOptions。触发器回调以文档优先:authTriggers、beforeCreate(data)、onCreate(doc)—— 第一个参数不是onUpdate(newDoc, oldDoc)。ctx - 内部鉴权函数位于(而非
internal.generated.*)。internal.auth.* - 异步mutation批处理是默认行为(代码生成已配置)。可按调用自定义:。启用同步模式:
execute({ batchSize, delayMs })或execute({ mode: 'sync' })。相关默认配置:defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } })、mutationBatchSize、mutationLeafBatchSize、mutationMaxRows。mutationScheduleCallCap - 多态联合是schema优先的:在中使用
convexTable(...)。查询配置不包含actionType: discriminator({ variants, as? })选项。写入保持扁平结构;读取时会合成嵌套的polymorphic(或自定义别名)。使用details可自动加载鉴别器表上的所有withVariants: true关联关系。one() - 默认情况下,不要在应用/插件代码中添加手动ORM mutation批处理循环。Convex运行时已处理mutation执行。优先使用基于集合的删除/更新而非逐行循环。仅在批处理外部副作用(例如Resend API调用)或有限清理扫描时添加显式分块逻辑。
Directory Boundary (Important)
目录边界(重要)
Use when the task needs:
references/setup/- Project/file structure setup → +
setup/index.mdsetup/server.md - Auth bootstrap →
setup/auth.md - Client/provider wiring →
setup/react.md - Framework-specific setup → or
setup/next.mdFor full template-level recreation: start withsetup/start.md, then load relevant setup files, then load selected feature refs.setup/index.md
当任务需要以下操作时,请使用:
references/setup/- 项目/文件结构搭建 → +
setup/index.mdsetup/server.md - 鉴权初始化 →
setup/auth.md - 客户端/提供者配置 →
setup/react.md - 框架特定搭建 → 或
setup/next.md如需完整模板级重建:从setup/start.md开始,然后加载相关搭建文件,再加载选定的功能参考文档。setup/index.md
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.
先确定以下决策:
- 每个端点的鉴权级别:/
public/optionalAuth/auth。private - 数据约束:写入后必须始终满足哪些条件?
- 查询类型:列表、详情、关联加载、搜索或流组合。
- 分页模式:偏移量、游标、无限滚动。
- 副作用:触发器、调度函数还是内联mutation。
- UI使用方式:仅客户端钩子、RSC预取或仅服务端caller。
- 风险路径:未授权、禁止访问、资源不存在、冲突、速率限制。
Canonical File Targets
标准文件目标
Typical feature touches:
convex/functions/schema.tsconvex/functions/<feature>.ts- (only if middleware/procedure builder changes)
convex/lib/crpc.ts - (only if cRPC context/meta wiring changes)
src/lib/convex/crpc.tsx - feature UI files
src/** - or
convex/functions/http.tsfor HTTP endpointsconvex/routers/** - or scheduled handlers if needed
convex/functions/crons.ts
典型功能开发会涉及以下文件:
convex/functions/schema.tsconvex/functions/<feature>.ts- (仅当中间件/过程构建器变更时)
convex/lib/crpc.ts - (仅当cRPC上下文/元数据配置变更时)
src/lib/convex/crpc.tsx - 功能UI文件
src/** - 或
convex/functions/http.ts(用于HTTP端点)convex/routers/** - 或调度处理器(如有需要)
convex/functions/crons.ts
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.
- Schema + 索引 + 关联关系。
- 用于跨行约束的触发器钩子(如果触发器路径不稳定,则使用显式mutation端同步逻辑)。
- 带有严格输入/输出 + 鉴权 + 速率限制的过程。
- 使用cRPC配置的React钩子(query/mutation/infinite)。
- 可选:HTTP路由、调度钩子。
- 鉴权/错误/触发器行为测试。
Core Patterns
核心模式
1) Schema + Relations + Trigger
1) Schema + 关联关系 + 触发器
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:
- Index fields that power filters/order/search.
- relation paths need child FK indexes.
many() - 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
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关键规则:
- 为支持过滤/排序/搜索的字段创建索引。
- 关联路径需要子表外键索引。
many() - 触发器逻辑必须限制范围且无递归。
- 使用表默认值保证一致的写入行为。
- 完整ORM/查询边缘案例请参考。
references/features/orm.md
2) Procedure Builders + Middleware
2) 过程构建器 + 中间件
ts
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:
- Build ,
public,optional, andauthprocedure families once inprivate.convex/lib/crpc.ts - is client-visible via generated API metadata. Never put secrets there.
.meta(...) - Middleware receives server-only info. When procedures are built from your app
procedurehelper, standardgenerated/serverqueries, mutations, and actions inferexport constautomatically from file path + export name. Usemodule:functiononly to override or cover unusual export shapes..name("module:function") - Resolve session/user once in middleware. Do not re-fetch auth state in every procedure.
- Shared chains preserve mutation writer types on mutation procedures. If the middleware itself performs writes, type it as mutation-only with
c.middleware().c.middleware<MutationCtx>(...) - Keep deeper auth/runtime edge cases in and
references/setup/server.md.references/features/auth*.md
ts
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,
},
});
});构建器关键规则:
- 在中一次性构建
convex/lib/crpc.ts、public、optional和auth过程族。private - 可通过生成的API元数据被客户端可见。绝不要在其中存放敏感信息。
.meta(...) - 中间件接收仅服务端可见的信息。当过程从应用的
procedure助手构建时,标准的generated/serverqueries、mutations和actions会自动从文件路径+导出名称推断export const。仅在需要覆盖或处理特殊导出结构时使用module:function。.name("module:function") - 在中间件中一次性解析会话/用户信息。不要在每个过程中重复获取鉴权状态。
- 共享的链会保留mutation过程的写入器类型。如果中间件本身执行写入操作,请将其类型标记为仅mutation可用:
c.middleware()。c.middleware<MutationCtx>(...) - 更深入的鉴权/运行时边缘案例请参考和
references/setup/server.md。references/features/auth*.md
3) Query + Mutation Procedure Template
3) 查询 + 变更过程模板
ts
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:
- Root input must be .
z.object(...) - Use strict ; add
.input(...)only when needed..output(...) - Omit for no-value mutations.
.output(...) - Use the default mutation rate limit; add only for named bucket overrides.
.meta({ ratelimit: ... }) - Throw for expected outcomes.
CRPCError - Bound every list with , cursor, or
limit..paginated(...) - Move advanced query-builder shapes to .
references/features/orm.md
ts
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;
});过程关键规则:
- 根输入必须是。
z.object(...) - 使用严格的;仅在需要时添加
.input(...)。.output(...) - 无返回值的mutations需省略。
.output(...) - 使用默认的mutation速率限制;仅在需要覆盖命名桶时添加。
.meta({ ratelimit: ... }) - 预期结果抛出。
CRPCError - 每个列表查询必须通过、游标或
limit限制范围。.paginated(...) - 高级查询构建器结构请参考。
references/features/orm.md
3b) Inter-Procedure Composition
3b) 跨过程组合
Use:
- in queries/mutations.
create<Module>Handler(ctx) - in actions/HTTP routes.
create<Module>Caller(ctx) - for action procedures.
caller.actions.* - for scheduled procedures.
caller.schedule.* - Never /
ctx.runQuery/ctx.runMutationfor module procedures.ctx.runAction
使用:
- 在queries/mutations中使用。
create<Module>Handler(ctx) - 在actions/HTTP路由中使用。
create<Module>Caller(ctx) - 使用调用action过程。
caller.actions.* - 使用调用调度过程。
caller.schedule.* - 绝不要使用/
ctx.runQuery/ctx.runMutation调用模块过程。ctx.runAction
4) Query Modes (Use The Right One)
4) 查询模式(选择合适的模式)
- Default to object .
where - Use callback only when composition reads better than object form.
where - Predicate/filter callbacks require first plus explicit
.withIndex(...)/limit.maxScan - Full-text search uses and does not support
search: { index, query, filters }.orderBy - Cursor paging is only stable when the field is indexed.
orderBy - Advanced modes (, vector search, pipelines, aggregate indexes) live in
pageByKey.references/features/orm.md
- 默认使用对象形式的。
where - 仅当组合逻辑比对象形式更易读时,才使用回调形式的。
where - 断言/过滤回调必须先调用,并显式指定
.withIndex(...)/limit。maxScan - 全文搜索使用,不支持
search: { index, query, filters }。orderBy - 游标分页仅在字段已索引时才稳定。
orderBy - 高级模式(、向量搜索、管道、聚合索引)请参考
pageByKey。references/features/orm.md
5) Mutation Patterns (Most Used)
5) 变更模式(最常用)
- Use on inserts when caller needs created ids.
.returning(...) - Every update/delete path gets an explicit .
where(...) - Clear optional columns with .
unsetToken - Async mutation execution is the default; use only when atomic all-at-once behavior is required.
.execute({ mode: "sync" }) - 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
- 当调用者需要创建的ID时,在插入操作上使用。
.returning(...) - 每个更新/删除路径都必须有显式的。
where(...) - 使用清空可选列。
unsetToken - 异步mutation执行是默认行为;仅当需要原子性一次性执行时,才使用。
.execute({ mode: "sync" }) - 优先使用基于集合的删除/更新。仅在处理外部副作用或有限清理时添加分块逻辑。
- Upsert、冲突处理、mutation批处理和schema扩展边缘案例请参考。
references/features/orm.md
6) Error Model
6) 错误模型
Use this map consistently:
- : invalid input or business precondition.
BAD_REQUEST - : no session.
UNAUTHORIZED - : session exists, permission missing.
FORBIDDEN - : missing or inaccessible resource.
NOT_FOUND - : duplicate or conflicting write.
CONFLICT - : rate limit.
TOO_MANY_REQUESTS - : unexpected failures only.
INTERNAL_SERVER_ERROR - Add small custom payloads on
datawhen the client needs domain metadata like conflicting ids. Read them on the client fromCRPCError.error.data
Required tests:
- unauthenticated rejection
- permission rejection when relevant
- missing resource path
- conflict path when relevant
- rate-limited write path when relevant
请统一使用以下映射:
- :无效输入或业务前置条件不满足。
BAD_REQUEST - :无会话。
UNAUTHORIZED - :存在会话,但缺少权限。
FORBIDDEN - :资源不存在或无法访问。
NOT_FOUND - :重复或冲突写入。
CONFLICT - :触发速率限制。
TOO_MANY_REQUESTS - :仅用于意外失败。
INTERNAL_SERVER_ERROR - 当客户端需要领域元数据(如冲突ID)时,可在中添加小型自定义
CRPCError负载。客户端可从data读取这些数据。error.data
必测场景:
- 未认证请求被拒绝
- 相关权限被拒绝
- 资源不存在路径
- 冲突路径(如有相关场景)
- 速率限制写入路径(如有相关场景)
7) React Query Integration
7) React Query集成
Preconditions (must be true before writing/using code paths):
useCRPC()- Generated imports exist () from setup bootstrap.
@convex/api - Provider chain is mounted (inside QueryClient + Convex provider flow).
CRPCProvider - If bootstrap/provider prerequisites are missing, stop feature work and finish first.
references/setup/ - Backend state is project-local in , not
.convex/.~/.convex
useCRPC()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 for subscribed cRPC query paths.
queryClient.invalidateQueries - Use only for one-time fetches; refresh those with explicit
{ subscribe: false }/refetch.fetchQuery - Use to avoid unauthorized fetch churn.
skipUnauth: true - For pagination, use from
useInfiniteQuery.kitcn/react - Prefer typed helpers for cache read/write/fetch ops instead of manual keys.
queryKey(...) - For kitcn auth flows, prefer wrappers (not raw auth client calls) to avoid logout race errors. Raw Convex preset keeps the plain auth client path.
createAuthMutations(...) - For mutation toasts, prefer over
error.data?.message;error.messageis the cleandata.messagepayload.CRPCError - Prefer one global mutation
QueryClienttoast withonError/mutation.meta.errorMessagerather than copy-pastingskipErrorToastin every component.onError - Full client/RSC depth lives in .
references/features/react.md
前置条件(编写/使用代码路径前必须满足):
useCRPC()- 已存在生成的导入(),来自搭建初始化流程。
@convex/api - 已挂载提供者链(位于QueryClient + Convex提供者流程内部)。
CRPCProvider - 如果缺少初始化/提供者前置条件,请停止功能开发,先完成中的内容。
references/setup/ - 后端状态存储在项目本地的中,而非
.convex/。~/.convex
useCRPC()const crpc = useCRPC(); const projects = useQuery(crpc.project.listProjects.queryOptions({ cursor: null, limit: 20 })); const createProject = useMutation(crpc.project.createProject.mutationOptions());客户端关键默认值/差异点:
- 查询默认是实时的()。
subscribe: true - 对于已订阅的cRPC查询路径,绝不要使用。
queryClient.invalidateQueries - 仅在一次性获取数据时使用;使用显式的
{ subscribe: false }/refetch刷新这些数据。fetchQuery - 使用避免未授权请求的无效消耗。
skipUnauth: true - 分页请使用中的
kitcn/react。useInfiniteQuery - 优先使用类型化的助手进行缓存读取/写入/获取操作,而非手动键值。
queryKey(...) - 对于kitcn鉴权流程,优先使用包装器(而非原始鉴权客户端调用)以避免登出竞态错误。原生Convex预设使用普通鉴权客户端路径。
createAuthMutations(...) - 对于mutation提示,优先使用而非
error.data?.message;error.message是干净的data.message负载。CRPCError - 优先使用全局mutation的
QueryClient提示,结合onError/mutation.meta.errorMessage,而非在每个组件中重复编写skipErrorToast。onError - 完整的客户端/RSC深度内容请参考。
references/features/react.md
8) RSC Patterns (Next.js)
8) RSC模式(Next.js)
Choose one per use case:
- (preferred): non-blocking, hydrated, client owns data.
prefetch(...) - : blocking server-only logic (redirects/auth checks), not hydrated.
caller.* - : blocking + hydrated when server needs data immediately.
preloadQuery(...)
Do not render result on server and again on client for the same data path.
preloadQuery- must wrap all client components that consume prefetched queries.
HydrateClient - Next.js-specific setup and deeper hydration tradeoffs live in and
references/setup/next.md.references/features/react.md
根据使用场景选择一种:
- (推荐):非阻塞、注入数据、客户端拥有数据所有权。
prefetch(...) - :阻塞式仅服务端逻辑(重定向/鉴权检查),不注入数据。
caller.* - :阻塞式+注入数据,适用于服务端需要立即获取数据的场景。
preloadQuery(...)
不要在服务端渲染结果,同时在客户端再次渲染同一数据路径。
preloadQuery- 必须包裹所有使用预取查询的客户端组件。
HydrateClient - Next.js特定搭建和更深入的注入权衡请参考和
references/setup/next.md。references/features/react.md
9) HTTP Route Pattern (When Feature Needs REST/Webhooks)
9) HTTP路由模式(当功能需要REST/Webhooks时)
ts
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 for search params.
z.coerce.* - Keep auth and permission checks in middleware/procedure.
- Apply rate limits to public/heavy endpoints.
- Validate webhook signatures before any side effects.
- Use /
publicRoute/authRoutebuilders fromoptionalAuthRoute.convex/lib/crpc.ts - Compose endpoints with for feature-level HTTP grouping.
router(...) - Client calls must pass path/query args as ; query values are strings.
{ params, searchParams } - Webhooks, streaming, and Hono-specific patterns live in .
references/features/http.md
ts
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特定规则:
- 对搜索参数使用。
z.coerce.* - 将鉴权和权限检查放在中间件/过程中。
- 对公共/高负载端点应用速率限制。
- 在执行任何副作用前验证Webhook签名。
- 使用中的
convex/lib/crpc.ts/publicRoute/authRoute构建器。optionalAuthRoute - 使用组合端点,实现功能级HTTP分组。
router(...) - 客户端调用必须将路径/查询参数作为传递;查询值为字符串类型。
{ params, searchParams } - Webhooks、流和Hono特定模式请参考。
references/features/http.md
10) Scheduling Pattern (If Needed)
10) 调度模式(如有需要)
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 directly only when you must schedule non-procedure
ctx.scheduler.*functions.internal.* - Cron expressions and operational details live in .
references/features/scheduling.md
示例:
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 });调度规则:
- 鉴权上下文不会自动传递;需显式传递用户/组织ID。
- Mutation调度与mutation事务是原子性的。
- 当需要取消任务时,存储返回的作业ID。
- 在actions内部调度的任务与action失败不具有原子性。
- Cron调度在UTC时区运行。
- 仅当必须调度非过程的函数时,才直接使用
internal.*。ctx.scheduler.* - Cron表达式和操作细节请参考。
references/features/scheduling.md
11) Testing Baseline (High Signal)
11) 测试基准(高价值)
Minimum feature test set:
- happy path query/mutation
- unauthenticated rejection ()
UNAUTHORIZED - permission/ownership rejection (where relevant)
FORBIDDEN - 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.
功能测试最小集合:
- 查询/mutation正常路径
- 未认证请求被拒绝()
UNAUTHORIZED - 权限/所有权被拒绝(相关场景下的)
FORBIDDEN - 资源不存在()
NOT_FOUND - 触发器副作用断言
- 若功能包含调度任务,则需断言调度逻辑
- 资源不存在检查应使用真实ID或非ID查找键(slug/名称/邮箱),而非合成ID
- 完整测试方案请参考。
references/features/testing.md - 如果Convex初始化阻碍集成测试,请提取纯守卫/辅助函数,待初始化完成后保留一个冒烟集成测试。
Performance + Safety Checklist
性能 + 安全检查清单
Before calling a feature done:
- Every list query is bounded (/cursor).
limit - Filters/order align with indexes.
- Expensive post-fetch logic uses pre-narrowed index path.
- Mutations use targeted and avoid accidental full scans.
where - 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.
- is not used on paths that rely on ORM constraints/RLS.
ctx.db - Paginated endpoints use + ORM cursor flow (not ad-hoc wrappers).
.paginated(...) - For any predicate/full-scan-like path, + bound (
.withIndex(...)/limit) is explicit.maxScan - NEVER use , no global lint-rule downgrades, no unresolved lint warnings in touched files.
@ts-nocheck
在标记功能完成前,请确认:
- 每个列表查询都已限制范围(/游标)。
limit - 过滤/排序与索引对齐。
- 昂贵的获取后逻辑使用预先缩小范围的索引路径。
- Mutations使用针对性的,避免意外全表扫描。
where - 触发器逻辑有限制、幂等且避免循环调用。
- 错误码明确且符合预期。
- 用户可见的写入操作已配置速率限制元数据。
- 测试覆盖鉴权+资源不存在+副作用场景。
- 依赖ORM约束/RLS的路径未使用。
ctx.db - 分页端点使用+ ORM游标流程(而非临时包装器)。
.paginated(...) - 对于任何断言/类全表扫描路径,已显式调用+ 限制(
.withIndex(...)/limit)。maxScan - 绝不要使用,不要全局降级 lint 规则,不要保留修改文件中未解决的 lint 警告。
@ts-nocheck
Common Mistakes (And Fixes)
常见错误(及修复方案)
| Mistake | Correct pattern |
|---|---|
| Raw Convex handler for new feature procedures | cRPC builders ( |
| 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 |
| Use object form: |
Using | Use |
Throwing generic | Throw |
| Infinite list with TanStack native hook directly | Use |
Primitive root input ( | Use root |
Returning nothing with | Omit explicit output |
| Manual pagination wrappers for infinite endpoints | Use |
Synthetic Convex IDs in tests ( | Use inserted IDs or semantic lookup keys |
| Aggregates disabled but helper/config still present | Remove aggregate helper + |
Putting secrets in | Keep metadata non-sensitive (client-visible) |
Using | Use |
Using | Use |
Adding | NEVER do this; fix the underlying types using canonical patterns in |
| Relaxing lint rules to pass checks | Keep baseline lint config; fix code-level warnings/errors instead |
| 错误操作 | 正确模式 |
|---|---|
| 新功能过程使用原生Convex处理器 | 使用cRPC构建器( |
| 写入时副作用在多个mutations中重复实现 | 使用Schema触发器,或当触发器路径不安全时,使用一个集中式的mutation端同步辅助函数 |
| 列表/搜索未限制范围 | 添加 |
| 使用对象形式: |
对策略敏感的读取使用 | 使用 |
预期结果抛出通用 | 抛出带有明确错误码的 |
| 直接使用TanStack原生钩子实现无限列表 | 使用 |
根输入为原始类型( | 使用根 |
使用 | 省略显式输出 |
| 为无限端点手动编写分页包装器 | 使用 |
测试中使用合成Convex ID( | 使用插入的ID或语义查找键 |
| 聚合功能已禁用但辅助函数/配置仍存在 | 移除聚合辅助函数 + |
在 | 保持元数据非敏感(客户端可见) |
直接使用 | 在queries/mutations中使用 |
在query/mutation上下文中使用 | 使用 |
添加 | 绝不要这样做;使用 |
| 放宽lint规则通过检查 | 保持基准lint配置;修复代码级警告/错误 |
Reference Escalation Map (Load Only If Needed)
参考文档升级映射(仅在需要时加载)
Setup (once per project):
- : bootstrap, env, decision intake, gates, checklist, troubleshooting
references/setup/index.md - : core backend (schema, ORM, cRPC) + optional module gates
references/setup/server.md - : auth core bootstrap + plugin setup
references/setup/auth.md - : client core (QueryClient, provider, cRPC context)
references/setup/react.md - : Next.js App Router setup
references/setup/next.md - : TanStack Start setup
references/setup/start.md - : skill/docs sync contract
references/setup/doc-guidelines.md
Features (per session, self-contained):
- : full ORM API, constraints, RLS, advanced mutations, filtering/search/composition/pagination
references/features/orm.md - : full client, RSC, hydration, error handling matrix
references/features/react.md - : typed REST routes, webhooks, streaming
references/features/http.md - : cron + delayed job patterns
references/features/scheduling.md - : deeper testing scenarios
references/features/testing.md - : aggregate component patterns
references/features/aggregates.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
references/features/migrations.mdCLI commands. Skip for backward-compatible changes (new optional fields, new tables, code-level defaults).kitcn migrate - : canonical plugin authoring patterns (split package entries, token config, scaffold/lockfile/CLI manifest rules). Load when: creating or refactoring plugins.
references/features/create-plugins.md - : full Better Auth core flow
references/features/auth.md - : admin plugin details
references/features/auth-admin.md - : org/multi-tenant plugin details
references/features/auth-organizations.md
搭建(每个项目一次):
- :初始化、环境、决策梳理、准入条件、检查清单、故障排查
references/setup/index.md - :核心后端(schema、ORM、cRPC)+ 可选模块准入条件
references/setup/server.md - :鉴权核心初始化 + 插件搭建
references/setup/auth.md - :核心客户端(QueryClient、提供者、cRPC上下文)
references/setup/react.md - :Next.js App Router搭建
references/setup/next.md - :TanStack Start搭建
references/setup/start.md - :技能/文档同步约定
references/setup/doc-guidelines.md
功能(每次会话,独立内容):
- :完整ORM API、约束、RLS、高级mutations、过滤/搜索/组合/分页
references/features/orm.md - :完整客户端、RSC、数据注入、错误处理矩阵
references/features/react.md - :类型化REST路由、Webhooks、流
references/features/http.md - :Cron + 延迟任务模式
references/features/scheduling.md - :深入测试场景
references/features/testing.md - :聚合组件模式
references/features/aggregates.md - :内置在线数据迁移(defineMigration、CLI、部署、漂移)。当任务涉及数据回填、可选→必填字段强化、字段重命名/删除、类型缩小或
references/features/migrations.mdCLI命令时加载。向后兼容变更(新增可选字段、新增表、代码级默认值)无需加载。kitcn migrate - :标准插件开发模式(拆分包入口、令牌配置、脚手架/锁文件/CLI清单规则)。创建或重构插件时加载。
references/features/create-plugins.md - :完整Better Auth核心流程
references/features/auth.md - :admin插件细节
references/features/auth-admin.md - :组织/多租户插件细节
references/features/auth-organizations.md