kitcn について
設定済みのkitcnアプリで機能を実装するためのコーディングスキルです。スキーマ、cRPCプロシージャ、UI、認可、テストを案内し、初期設定や専門的な作業は別のリファレンスへ誘導します。
主な機能
- 01スキーマ、インデックス、リレーション、トリガーの指針
- 02cRPCプロシージャのパターン
- 03認可とレート制限の確認
- 04TanStack Queryを使ったReact UI
- 05範囲とインデックスを意識したデータアクセス
- 06重要なテストに絞った指針
活用シーン
- 01設定済みkitcnアプリへの機能追加
- 02認可付きデータクエリとミューテーションの実装
- 03cRPCを利用するUIの構築
- 04認可処理と副作用のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ udecode/ kitcn --skill kitcnインストール後、Claude Code で/kitcnを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/features/aggregates.mdauth-admin.mdauth-organizations.mdauth-polar.mdauth.mdcreate-plugins.mdhttp.mdmigrations.mdorm.mdratelimit.mdreact.mdscheduling.mdtesting.mdreferences/setup/auth.mdbiome.mddoc-guidelines.mdexpo.mdindex.mdnext.mdreact.mdserver.mdstart.md詳細
kitcn コアスキル(80% のケース)
設定済みの kitcn アプリで日常的な機能を実装する場合は、まずこのファイルを使用してください。
- セットアップ/ブートストラップ/環境変数/認証の配線、またはプロジェクト構造のミラーリングが不足している場合は、
references/setup/index.md(続いて該当するセットアップファイル)を使用してください。 - 高度なタスクやニッチなタスクの場合は、末尾に記載されている該当機能のリファレンスのみを読み込んでください。
対象範囲
対象:
- スキーマのテーブル、インデックス、リレーション、トリガーの追加または更新。
- ランタイム認証とレート制限を適用した cRPC プロシージャ(
query、mutation、action、httpAction)の実装。 useCRPC()と TanStack Query を使用した機能 UI の実装。- 認証、エラー、副作用を対象とした、最小限かつ重要度の高いテストの追加。 対象外:
- 新規プロジェクトのセットアップ/インストール/環境変数設定/ブートストラップ。
- プラグインの詳細な解説(admin/organizations/polar)。
- 内部パッケージレベルの同等性テスト。
スキルの契約
- アプリデータへのアクセスには
ctx.ormを優先してください。 - リスト取得/読み取りの処理は、上限を設け、インデックスを考慮してください。
- cRPC ビルダーとミドルウェアを使用し、新しい機能コードでは生のハンドラーオブジェクトを避けてください。
- 想定されるエラーには
CRPCErrorを使用してください。 - 複数行にまたがる不変条件にはスキーマトリガーを優先します。ただし、トリガーの実行が不安定な場合(たとえば、初期化/シード処理が停止する場合や再帰的な書き込み経路がある場合)は、不変条件の維持を明示的な mutation ヘルパーに移してください。
- 認証/レート制限のチェックはサーバー側で行ってください。
- プロシージャ間呼び出し:生成されたランタイムヘルパーを使用してください。クエリ/ミューテーションでは
create<Module>Handler(ctx)、アクション/HTTP ルートではcreate<Module>Caller(ctx)、アクションプロシージャではcaller.actions.*、スケジューリングではcaller.schedule.*を使用してください。モジュールプロシージャの呼び出しにctx.runQuery/ctx.runMutation/ctx.runActionを直接使用しないでください。
ショートカットモード(tRPC + Drizzle のメンタルモデル)
デフォルトの前提:
- cRPC の動作は tRPC に似ています(ビルダーチェーン + ミドルウェア + TanStack のオプション)。
- ORM の動作は Drizzle に似ています(スキーマ、リレーション、
findMany/findFirst、insert/update/delete)。 次の相違点だけを覚えておいてください:
- プロシージャの入力ルートは
z.object(...)である必要があります(プリミティブ型のルート引数は不可)。 z.void()の出力は使えません。値を返さないミューテーションでは.output(...)を省略してください。.output(...)はハンドラーの値をそのまま解析し、何も置き換えません。ハンドラーはスキーマの入力型を返す必要があるため、z.string().nullable()ではundefinedではなく明示的なnull(?? null)が必要です。値の不在は.nullable()で表現し、トップレベルの.optional()は使わないでください。Convex はundefinedをnullとして連携し、トップレベルの optional は表現できないため、.output(z.string().optional())はv.string()を公開し、ハンドラーがundefinedを返すとデプロイ時にnullが拒否されます。オブジェクト内の.optional()は問題ありません。zCustomQuery/zCustomMutation/zCustomActionの低レベルなreturns:オプションは異なり、解析前にundefinedをnullに置き換えます。- 複数の
.input(...)呼び出しは入力の形状をマージします。 .paginated({ limit, item })は.query()の前に置く必要があり、input.cursor、input.endCursor、input.limitを追加します。ライブ分割を上限内に保つため、ORM のカーソルクエリには3つすべてを渡してください。- メタデータは
@convex/apiのリーフ(api.namespace.fn.meta)にコード生成されるため、.meta(...)にシークレットを入れないでください。.meta(...)のチェーンは浅いマージであり、defaultMetaをサポートします。 - 認証メタデータはクライアントの動作を制御します。
auth: "optional"は認証の読み込み完了を待ってから実行し、auth: "required"は読み込み完了を待ってからログアウト中ならスキップします。 ctx.ormは制約と RLS を適用します。ctx.dbはそれらを迂回します。- ページネーションなしの
findMany()では、サイズを明示する必要があります(limit、カーソルモード、スキーマのdefaultLimit、または明示的なallowFullScan)。 - 述語を使う
whereには、明示的な.withIndex(...)が必要です。暗黙の全件スキャンへのフォールバックはありません。 - カーソルページネーションは最初の
orderByフィールドを使用します。安定したページングのため、そのフィールドにインデックスを付けてください。 maxScanはカーソルモードにのみ適用されます。allowFullScanはカーソルを使わない全件スキャンを明示的に許可するためのものです。- 文字列演算子、
columnsによるプロジェクション、多リレーションのサブフィルターは、フェッチ後に実行される場合があります。結果サイズは早い段階で制限してください。 - 検索モードでは関連度順になり、
orderByはサポートされません。ベクトルモードにはさらに厳しい制限があります(カーソル/オフセット/トップレベルの where/order は不可)。 whereを指定せずに更新/削除すると、allowFullScan()を指定していない限りエラーになります。count()、aggregate()、groupBy()には、一致するaggregateIndexが必要です。複数の.count()呼び出しやfindMany+ 手動の JavaScript グループ化ではなく、groupBy({ by, _count, _sum })を使ってください。すべてのbyフィールドはwhereで有限の値に制約されている必要があります(eq/in/isNull)。references/features/aggregates.mdを参照してください。- cRPC の React クエリはデフォルトでリアルタイムです(
subscribe: true)。これらの購読対象パスにはqueryClient.invalidateQueriesを決して使わないでください。 - RSC では、
prefetchはクライアントをハイドレートし、callerはサーバー専用でハイドレートされず、preloadQueryはハイドレートしますが、クライアント側でもレンダリングすると分割の所有権が古くなることがあります。 - Better Auth の Next.js 用ショートカットは
convexBetterAuth(...)です。汎用のサーバー専用ショートカットはcreateCallerFactory(...)です。 - kitcn の認証クライアントパスでは、ログアウト前に認証クエリの購読を解除するため、
createAuthMutations(authClient)ラッパーを使ってください。Raw Convex プリセットでは、より小規模でシンプルなauthClientを使います。 - モジュール間の呼び出しに
ctx.runQuery/ctx.runMutation/ctx.runActionを直接決して使わないでください。convex/functions/generated/<module>.runtimeの生成済みランタイムヘルパーを使ってください。 - クエリ/ミューテーションでは、デフォルトで
create<Module>Handler(ctx)を使います。オーバーヘッドはゼロで、クエリ/ミューテーションのコンテキストだけを使い、冗長な検証やミドルウェアはありません。 create<Module>Caller(ctx)はアクションと HTTP ルートで使います。アクションプロシージャはcaller.actions.*の下にあり、スケジューリングはcaller.schedule.now|after|at|cancelの下にあります。実際のActionCtxコールバックに限りrequireActionCtx(ctx)を使い、ミューテーションまたはアクションのコンテキストからスケジュールできる場合はrequireSchedulerCtx(ctx)を使ってください。各 caller/handler はモジュールを即時に読み込むため、大きなモジュールは分割してください。- API 型(
Api、ApiInputs、ApiOutputs、Select、Insert、TableName)は@convex/apiからインポートします。inferApiInputs<typeof api>を手動で使う必要はありません。 - HTTP ルーターは、コード生成のために
appRouterではなくhttpRouterとしてエクスポートする必要があります。 - サーバー配線のインポート元は
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.auth.*ではなくinternal.generated.*にあります。 - 非同期ミューテーションのバッチ処理がデフォルトです(コード生成時に配線されます)。呼び出しごとにカスタマイズするには、
execute({ batchSize, delayMs })を使います。同期処理にするには、execute({ mode: 'sync' })またはdefineSchema(..., { defaults: { mutationExecutionMode: 'sync' } })を指定します。関連するデフォルト値はmutationBatchSize、mutationLeafBatchSize、mutationMaxRows、mutationScheduleCallCapです。 - 多相ユニオンはスキーマ優先です。
convexTable(...)でactionType: discriminator({ variants, as? })を使ってください。クエリ設定にpolymorphicオプションはありません。書き込みはフラットなままで、読み取りではネストされたdetails(またはカスタムエイリアス)が合成されます。判別子テーブルのone()リレーションをすべて自動読み込みするには、withVariants: trueを使ってください。 - アプリ/プラグインのコードでは、デフォルトで ORM のミューテーションバッチ処理ループを手動で追加しないでください。Convex ランタイムのバッチ処理が、すでにミューテーションの実行を処理します。行ごとのループより、集合ベースの削除/更新を優先してください。明示的な分割を追加するのは、外部副作用をまとめる場合(たとえば Resend API の呼び出し)や、上限付きのクリーンアップ処理を行う場合に限ります。
ディレクトリの境界
次のタスクでは references/setup/ を使用します。
- プロジェクト/ファイル構成のセットアップ →
setup/index.md+setup/server.md - 認証の初期設定 →
setup/auth.md - クライアント/プロバイダーの接続 →
setup/react.md - フレームワーク固有のセットアップ →
setup/next.mdまたはsetup/start.mdテンプレート全体を再現する場合:まずsetup/index.mdを読み込み、次に関連するセットアップファイルを読み込み、その後、選択した機能リファレンスを読み込みます。
最初の機能要件の確認(編集前に実施)
まず、次の項目を決定します。
- エンドポイントごとの認証レベル:
public/optionalAuth/auth/private。 - データの不変条件:書き込み後に常に満たすべき条件は何か。
- クエリの形:一覧、詳細、リレーション読み込み、検索、ストリームの組み合わせのいずれか。
- ページネーション方式:オフセット、カーソル、無限。
- 副作用:トリガー、スケジュール関数、インラインミューテーションのいずれか。
- UI での利用方法:クライアントフックのみ、RSC プリフェッチ、サーバー専用の呼び出しのいずれか。
- リスクのあるパス:未認証、アクセス禁止、見つからない場合、競合、レート制限。
基本的なファイルの配置先
一般的な機能で変更するファイル:
convex/functions/schema.tsconvex/functions/<feature>.tsconvex/lib/crpc.ts(ミドルウェア/プロシージャビルダーを変更する場合のみ)src/lib/convex/crpc.tsx(cRPC のコンテキスト/メタデータの接続を変更する場合のみ)src/**機能の UI ファイル- HTTP エンドポイントには
convex/functions/http.tsまたはconvex/routers/** - 必要に応じて
convex/functions/crons.tsまたはスケジュールハンドラー
E2E のデフォルトの構築順序
- スキーマ、インデックス、リレーション。
- 行をまたぐ不変条件のトリガーフック(または、トリガーパスが不安定な場合は、明示的なミューテーション側の同期)。
- 厳密な入出力、認証、レート制限を備えたプロシージャ。
- cRPC オプションを使用する React フック(クエリ/ミューテーション/無限)。
- 任意:HTTP ルート、スケジュールフック。
- 認証/エラー/トリガー動作のテスト。
基本パターン
1) スキーマ + リレーション + トリガー
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 });
},
},
});
重要なスキーマのルール:
- フィルター/並べ替え/検索に使うフィールドにはインデックスを作成します。
many()リレーションのパスには、子の外部キーインデックスが必要です。- トリガーロジックは処理範囲を限定し、再帰しないようにします。
- 一貫した書き込み動作のために、テーブルのデフォルト値を使用します。
- ORM/クエリの詳しいエッジケースについては、
references/features/orm.mdを参照してください。
2) プロシージャビルダー + ミドルウェア
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 },
});
});
重要なビルダーのルール:
convex/lib/crpc.tsでpublic、optional、auth、privateのプロシージャファミリーを一度だけ構築します。認証済みアクションのビルダーはconvex/lib/crpc-action.tsに配置します。getAuthをインポートするビルダーモジュールはこのファイルのみです。.meta(...)は生成された API メタデータを介してクライアントから参照できます。秘密情報は決して含めないでください。- ミドルウェアにはサーバー専用の
procedure情報が渡されます。アプリのgenerated/serverヘルパーからプロシージャを構築する場合、標準のexport constクエリ、ミューテーション、アクションでは、ファイルパスとエクスポート名からmodule:functionが自動的に推論されます。.name("module:function")を使うのは、上書きする場合、または特殊なエクスポート形式に対応する場合のみです。 - ミドルウェアでセッション/ユーザーを一度だけ解決します。各プロシージャで認証状態を再取得しないでください。
クエリ/ミューテーションのミドルウェアでは
kitcn/authのgetSession(ctx)を使用します。これはセッション行を直接読み取ります。convex/lib/crpc.tsではgetAuth(ctx)を使用しないでください。これを使うと Better Auth の定義全体とすべての認証プラグインが、すべてのプロシージャモジュールの静的インポートクロージャに含まれます。Convex にはこれを回避する動的なimport()がありません。getAuthをインポートするのは、auth.api.*を呼び出すモジュール(convex/lib/crpc-action.ts、HTTP ルート、組織/管理者向けミューテーション)のみにしてください。 - 共有の
c.middleware()チェーンでは、ミューテーションプロシージャのミューテーションライター型が維持されます。ミドルウェア自体が書き込みを行う場合は、c.middleware<MutationCtx>(...)を指定して、ミューテーション専用として型付けします。 - 認証/ランタイムに関する詳しいエッジケースについては、
references/setup/server.mdおよびreferences/features/auth*.mdを参照してください。
3)クエリ+ミューテーションの手順テンプレート
import * as z from "zod";
import { eq } from "kitcn/orm";
import { CRPCError } from "kitcn/server";
import { authMutation, authQuery } from "../lib/crpc";
import { project } from "./schema";
export const listProjects = authQuery
.paginated({ limit: z.number().min(1).max(50).default(20), item: project })
.query(async ({ ctx, input }) =>
ctx.orm.query.project.findMany({
where: { ownerId: ctx.userId },
orderBy: { updatedAt: "desc" },
cursor: input.cursor,
endCursor: input.endCursor,
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(...)を追加します。 - 値を返さないミューテーションでは
.output(...)を省略します。 - デフォルトのミューテーションレート制限を使用し、名前付きバケットのオーバーライドに限り
.meta({ ratelimit: ... })を追加します。 - 想定される結果には
CRPCErrorをスローします。 - すべてのリストに
limit、カーソル、または.paginated(...)を設定します。 - 高度なクエリビルダーの形式は
references/features/orm.mdに移します。
3b)プロシージャ間の合成
次を使用します。
- クエリ/ミューテーションでは
create<Module>Handler(ctx)。 - アクション/HTTP ルートでは
create<Module>Caller(ctx)。 - アクションプロシージャでは
caller.actions.*。 - スケジュール済みプロシージャでは
caller.schedule.*。 - モジュールプロシージャに
ctx.runQuery/ctx.runMutation/ctx.runActionを使用しないでください。
4)クエリモード(適切なものを使用)
- デフォルトではオブジェクト形式の
whereを使用します。 - コールバック形式の
whereは、オブジェクト形式よりも合成の見通しがよい場合にのみ使用します。 - 述語/フィルターのコールバックでは、先に
.withIndex(...)を指定し、さらにlimit/maxScanを明示する必要があります。 - 全文検索では
search: { index, query, filters }を使用し、orderByはサポートされません。 - カーソルページングが安定するのは、
orderByフィールドにインデックスが設定されている場合のみです。 - 高度なモード(
pageByKey、ベクトル検索、パイプライン、集約インデックス)はreferences/features/orm.mdに記載されています。
5)ミューテーションのパターン(最もよく使うもの)
- 呼び出し元が作成された ID を必要とする場合、挿入時に
.returning(...)を使用します。 - すべての更新/削除パスで、明示的な
where(...)を指定します。 - オプションの列をクリアするには
unsetTokenを使用します。 - 非同期のミューテーション実行がデフォルトです。原子的にすべてを一括処理する必要がある場合にのみ
.execute({ mode: "sync" })を使用します。 - セットベースの削除/更新を優先します。外部の副作用や、対象を限定したクリーンアップにのみチャンク処理を追加します。
- Upsert、競合処理、ミューテーションのバッチ処理、スキーマ拡張に関するエッジケースは
references/features/orm.mdに記載されています。
6)エラーモデル
次の対応関係を一貫して使用します。
BAD_REQUEST:無効な入力またはビジネス上の前提条件の不成立。UNAUTHORIZED:セッションがありません。FORBIDDEN:セッションはありますが、権限がありません。NOT_FOUND:リソースが存在しないか、アクセスできません。CONFLICT:重複または書き込みの競合。TOO_MANY_REQUESTS:レート制限。INTERNAL_SERVER_ERROR:予期しない失敗にのみ使用します。cRPC は.output(...)の解析に失敗した場合にもこれを発生させます。その際のメッセージはOutput validation failedで、サニタイズされた構造化 Zod イシューはerror.data.ZodErrorに含まれます。ハンドラーの出力を含む可能性があるカスタムイシューメッセージとフィールドは、サーバー側に留まります。- クライアントが競合する ID などのドメインメタデータを必要とする場合は、
CRPCErrorに小さなカスタムdataペイロードを追加します。クライアントではerror.dataから読み取ります。
必須テスト:
- 未認証時の拒否
- 該当する場合の権限不足による拒否
- リソースが見つからない場合のパス
- 該当する場合の競合パス
- 該当する場合のレート制限対象の書き込みパス
7) React Query の統合
前提条件(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を使用しないでください。 - 1 回限りのフェッチには
{ subscribe: false }のみを使用し、明示的なrefetch/fetchQueryで更新してください。 - 認証されていない状態での不要なフェッチの繰り返しを避けるには、
skipUnauth: trueを使用してください。 - ページネーションには、
kitcn/reactのuseInfiniteQueryを使用してください。 - キャッシュの読み取り/書き込み/フェッチ操作では、手動でキーを指定するより、型付きの
queryKey(...)ヘルパーを優先してください。 - kitcn の認証フローでは、ログアウト時の競合エラーを避けるため、生の認証クライアント呼び出しではなく、
createAuthMutations(...)ラッパーを優先してください。Raw Convex プリセットでは、通常の認証クライアントパスを使用します。 - ミューテーションのトーストには、
error.messageよりerror.data?.messageを優先してください。data.messageはクリーンなCRPCErrorペイロードです。 - すべてのコンポーネントに
onErrorをコピー&ペーストするより、mutation.meta.errorMessage/skipErrorToastを使用するグローバルなQueryClientミューテーションonErrorトーストを優先してください。 - クライアント/RSC の詳細については、
references/features/react.mdを参照してください。
8) RSC のパターン(Next.js)
ユースケースごとに、次のいずれかを選択してください。
prefetch(...)(推奨):ノンブロッキングでハイドレーションされ、クライアントがデータを所有します。caller.*:ブロッキングのサーバー専用ロジック(リダイレクト/認証チェック)で、ハイドレーションされません。preloadQuery(...):サーバーがデータをすぐに必要とする場合の、ブロッキングかつハイドレーションされる方法です。
同じデータパスについて、preloadQuery の結果をサーバーとクライアントの両方でレンダリングしないでください。
- プリフェッチ済みクエリを使用するすべてのクライアントコンポーネントは、
HydrateClientでラップする必要があります。 - Next.js 固有のセットアップと、ハイドレーションのトレードオフの詳細については、
references/setup/next.mdとreferences/features/react.mdを参照してください。
9) HTTP ルートのパターン(REST/Webhook が必要な場合)
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ビルダーを使用してください。router(...)を使用して、機能レベルで HTTP エンドポイントをグループ化してください。- クライアント呼び出しでは、パス/クエリ引数を
{ params, searchParams }として渡してください。クエリ値は文字列です。 - Webhook、ストリーミング、Hono 固有のパターンについては、
references/features/http.mdを参照してください。
10) スケジューリングのパターン(必要な場合)
例: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/組織 ID を明示的に渡してください。
- ミューテーションのスケジューリングは、ミューテーションのトランザクションと原子的に行われます。
- キャンセルが必要な場合は、返されたジョブ ID を保存してください。
- アクション内のスケジューリングは、アクションの失敗に対して原子的ではありません。
- Cron スケジュールは UTC で実行されます。
- プロシージャー以外の
internal.*関数をスケジュールする必要がある場合に限り、ctx.scheduler.*を直接使用してください。 - Cron 式と運用上の詳細については、
references/features/scheduling.mdを参照してください。
11) テストの基準(重要度が高い項目)
機能の最小テストセット:
- 正常系のクエリ/ミューテーション
- 未認証時の拒否(
UNAUTHORIZED) - 権限/所有権がない場合の拒否(該当する場合は
FORBIDDEN) - リソースが見つからない場合(
NOT_FOUND) - 副作用が発生することのアサーション
- 機能が処理をスケジュールする場合は、スケジューラーのアサーション
- 見つからないことの確認には、合成した ID ではなく、実際の ID または ID 以外の検索キー(slug/name/email)を使用する
- テストの詳しい手順は
references/features/testing.mdにあります。 - Convex のブートストラップが結合テストを妨げる場合は、純粋なガード/ヘルパーを切り出し、ブートストラップが機能するようになったらスモーク結合テストを 1 つ実施します。
パフォーマンスと安全性のチェックリスト
機能の完成と判断する前に、次を確認してください。
- すべてのリストクエリに上限(
limit/カーソル)が設定されています。 - フィルター/並び順がインデックスに沿っています。
- 負荷の高いフェッチ後のロジックでは、事前に絞り込んだインデックスパスを使用しています。
- ミューテーションでは対象を絞った
whereを使用し、意図しない全件スキャンを避けています。 - トリガーロジックに上限があり、冪等で、ピンポンループを回避しています。
- エラーコードが明示され、意図的に選択されています。
- ユーザー向けの書き込みにはレート制限のメタデータが設定されています。
- テストで認証、見つからない場合、副作用をカバーしています。
- ORM の制約/RLS に依存するパスでは
ctx.dbを使用していません。 - ページネーションを使用するエンドポイントでは、独自のラッパーではなく
.paginated(...)と ORM のカーソルフローを使用しています。 - 述語/全件スキャンに相当するパスでは、
.withIndex(...)と上限(limit/maxScan)を明示しています。 @ts-nocheckは絶対に使用せず、グローバルな lint ルールを緩和せず、変更したファイルに未解決の lint 警告を残しません。
よくある間違い(および修正方法)
| 間違い | 正しいパターン |
|---|---|
| 新機能のプロシージャに Convex ハンドラーを直接使う | cRPC ビルダー(publicQuery、authMutation など) |
| 書き込み時の副作用が複数のミューテーションで重複している | スキーマトリガーを使うか、トリガー経由が安全でない場合は、ミューテーション側の同期ヘルパーを1か所に集約します |
| リストや検索に上限がない | limit とカーソル/ページネーションを追加します |
orderBy を配列オブジェクトとして記述する | オブジェクト形式を使います:orderBy: { updatedAt: "desc" } |
ポリシーの影響を受ける読み取りに ctx.db を使う | ctx.orm を使います(RLS/制約の経路) |
想定される結果に汎用的な Error を投げる | 明示的なコードを指定して CRPCError を投げます |
| 無限リストに TanStack のネイティブフックを直接使う | kitcn/react の useInfiniteQuery を使います |
プリミティブ型をルート入力にする(z.string()) | ルートに z.object(...) の入力スキーマを使います |
z.void() で何も返さない | 明示的な出力指定を省略します |
存在しない可能性のある検索結果を .output(...nullable()) で返す | ?? null で補完します。.output(...) は undefined に対して何も代入しません |
| 無限エンドポイントに手動のページネーションラッパーを使う | .paginated({ limit, item }) を使います |
テストで合成 Convex ID("missing-id")を使う | 挿入した ID または意味のある検索キーを使います |
| 集計を無効にしているのにヘルパーや設定が残っている | 集計ヘルパー、defineTriggers ハンドラー、アプリ設定をまとめて削除します |
.meta(...) にシークレットを入れる | メタデータには機密情報を含めません(クライアントから見えるため) |
ctx.runQuery/ctx.runMutation/ctx.runAction を直接使う | クエリ/ミューテーションでは create<Module>Handler(ctx) を、アクション/HTTP では caller.actions.*/caller.schedule.* とともに create<Module>Caller(ctx) を使います(generated/<module>.runtime から) |
クエリ/ミューテーションのコンテキストで createCaller を使う | create<Module>Handler(ctx) を使います。オーバーヘッドなしで、冗長な検証を回避できます |
コンパイルを通すために // @ts-nocheck を追加する | 絶対に行わないでください。references/setup/ の標準パターンを使って、根本の型の問題を修正します |
| チェックを通すために lint ルールを緩和する | 基本の lint 設定を維持し、代わりにコードレベルの警告やエラーを修正します |
参照ドキュメントの読み込みマップ(必要な場合のみ読み込み)
セットアップ(プロジェクトごとに一度):
references/setup/index.md:ブートストラップ、環境、判断事項の確認、ゲート、チェックリスト、トラブルシューティングreferences/setup/server.md:コアバックエンド(スキーマ、ORM、cRPC)+オプションモジュールのゲートreferences/setup/auth.md:認証コアのブートストラップ+プラグインのセットアップreferences/setup/react.md:クライアントコア(QueryClient、プロバイダー、cRPC コンテキスト)references/setup/next.md:Next.js App Router のセットアップreferences/setup/start.md:TanStack Start のセットアップreferences/setup/doc-guidelines.md:スキルとドキュメントの同期に関する契約
機能(セッションごとに、単独で完結):
references/features/orm.md:ORM API 全般、制約、RLS、高度なミューテーション、フィルタリング/検索/構成/ページネーションreferences/features/react.md:クライアント、RSC、ハイドレーション、エラーハンドリングのマトリクス全般references/features/http.md:型付き REST ルート、Webhook、ストリーミングreferences/features/scheduling.md:cron+遅延ジョブのパターンreferences/features/testing.md:より踏み込んだテストシナリオreferences/features/aggregates.md:集約コンポーネントのパターンreferences/features/migrations.md:組み込みのオンラインデータマイグレーション(defineMigration、CLI、デプロイ、ドリフト)。次の場合に読み込みます:タスクにデータのバックフィル、オプションから必須へのフィールドの変更、フィールドの名前変更/削除、型の絞り込み、またはkitcn migrateCLI コマンドが関係する場合。後方互換性のある変更(新しいオプションフィールド、新しいテーブル、コードレベルのデフォルト)では読み込みません。references/features/create-plugins.md:プラグイン作成の標準パターン(パッケージエントリーの分割、トークン設定、スキャフォールド/ロックファイル/CLI マニフェストのルール)。プラグインを作成またはリファクタリングする場合に読み込みます。references/features/ratelimit.md:レート制限のランタイム計算(シャード予算の配分、check()とlimit()の違い、スナップショットの変換、読み取り精度、失敗モード)。shardsの調整、残りのクォータの読み取り、または予期しない拒否のデバッグ時に読み込みます。setup/server.mdの対象である単純なratelimit.middleware()の接続では読み込みません。references/features/auth.md:Better Auth のコアフロー全般references/features/auth-admin.md:管理者プラグインの詳細references/features/auth-organizations.md:組織/マルチテナントプラグインの詳細