kitcn 0.0.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/bin/intent.js +3 -0
  2. package/dist/aggregate/index.d.ts +388 -0
  3. package/dist/aggregate/index.js +37 -0
  4. package/dist/api-entry-BckXqaLb.js +66 -0
  5. package/dist/auth/client/index.d.ts +37 -0
  6. package/dist/auth/client/index.js +217 -0
  7. package/dist/auth/config/index.d.ts +45 -0
  8. package/dist/auth/config/index.js +24 -0
  9. package/dist/auth/generated/index.d.ts +2 -0
  10. package/dist/auth/generated/index.js +3 -0
  11. package/dist/auth/http/index.d.ts +64 -0
  12. package/dist/auth/http/index.js +461 -0
  13. package/dist/auth/index.d.ts +221 -0
  14. package/dist/auth/index.js +1398 -0
  15. package/dist/auth/nextjs/index.d.ts +50 -0
  16. package/dist/auth/nextjs/index.js +81 -0
  17. package/dist/auth-store-Cljlmdmi.js +197 -0
  18. package/dist/builder-CBdG5W6A.js +1974 -0
  19. package/dist/caller-factory-cTXNvYdz.js +216 -0
  20. package/dist/cli.mjs +13264 -0
  21. package/dist/codegen-lF80HSWu.mjs +3416 -0
  22. package/dist/context-utils-HPC5nXzx.d.ts +17 -0
  23. package/dist/create-schema-odyF4kCy.js +156 -0
  24. package/dist/create-schema-orm-DOyiNDCx.js +246 -0
  25. package/dist/crpc/index.d.ts +105 -0
  26. package/dist/crpc/index.js +169 -0
  27. package/dist/customFunctions-C0voKmtx.js +144 -0
  28. package/dist/error-BZEnI7Sq.js +41 -0
  29. package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
  30. package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
  31. package/dist/http-types-DqJubRPJ.d.ts +292 -0
  32. package/dist/meta-utils-0Pu0Nrap.js +117 -0
  33. package/dist/middleware-BUybuv9n.d.ts +34 -0
  34. package/dist/middleware-C2qTZ3V7.js +84 -0
  35. package/dist/orm/index.d.ts +17 -0
  36. package/dist/orm/index.js +10713 -0
  37. package/dist/plugins/index.d.ts +2 -0
  38. package/dist/plugins/index.js +3 -0
  39. package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
  40. package/dist/procedure-caller-MWcxhQDv.js +349 -0
  41. package/dist/query-context-B8o6-8kC.js +1518 -0
  42. package/dist/query-context-CFZqIvD7.d.ts +42 -0
  43. package/dist/query-options-Dw7cOyXl.js +121 -0
  44. package/dist/ratelimit/index.d.ts +269 -0
  45. package/dist/ratelimit/index.js +856 -0
  46. package/dist/ratelimit/react/index.d.ts +76 -0
  47. package/dist/ratelimit/react/index.js +183 -0
  48. package/dist/react/index.d.ts +1284 -0
  49. package/dist/react/index.js +2526 -0
  50. package/dist/rsc/index.d.ts +276 -0
  51. package/dist/rsc/index.js +233 -0
  52. package/dist/runtime-CtvJPkur.js +2453 -0
  53. package/dist/server/index.d.ts +5 -0
  54. package/dist/server/index.js +6 -0
  55. package/dist/solid/index.d.ts +1221 -0
  56. package/dist/solid/index.js +2940 -0
  57. package/dist/transformer-DtDhR3Lc.js +194 -0
  58. package/dist/types-BTb_4BaU.d.ts +42 -0
  59. package/dist/types-BiJE7qxR.d.ts +4 -0
  60. package/dist/types-DEJpkIhw.d.ts +88 -0
  61. package/dist/types-HhO_R6pd.d.ts +213 -0
  62. package/dist/validators-B7oIJCAp.js +279 -0
  63. package/dist/validators-vzRKjBJC.d.ts +88 -0
  64. package/dist/watcher.mjs +96 -0
  65. package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
  66. package/package.json +107 -34
  67. package/skills/convex/SKILL.md +486 -0
  68. package/skills/convex/references/features/aggregates.md +353 -0
  69. package/skills/convex/references/features/auth-admin.md +446 -0
  70. package/skills/convex/references/features/auth-organizations.md +1141 -0
  71. package/skills/convex/references/features/auth-polar.md +579 -0
  72. package/skills/convex/references/features/auth.md +470 -0
  73. package/skills/convex/references/features/create-plugins.md +153 -0
  74. package/skills/convex/references/features/http.md +676 -0
  75. package/skills/convex/references/features/migrations.md +162 -0
  76. package/skills/convex/references/features/orm.md +1166 -0
  77. package/skills/convex/references/features/react.md +657 -0
  78. package/skills/convex/references/features/scheduling.md +267 -0
  79. package/skills/convex/references/features/testing.md +209 -0
  80. package/skills/convex/references/setup/auth.md +501 -0
  81. package/skills/convex/references/setup/biome.md +190 -0
  82. package/skills/convex/references/setup/doc-guidelines.md +145 -0
  83. package/skills/convex/references/setup/index.md +761 -0
  84. package/skills/convex/references/setup/next.md +116 -0
  85. package/skills/convex/references/setup/react.md +175 -0
  86. package/skills/convex/references/setup/server.md +473 -0
  87. package/skills/convex/references/setup/start.md +67 -0
  88. package/LICENSE +0 -21
  89. package/README.md +0 -0
  90. package/dist/index.d.mts +0 -5
  91. package/dist/index.d.mts.map +0 -1
  92. package/dist/index.mjs +0 -6
  93. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,676 @@
1
+ # HTTP Router
2
+
3
+ Typed REST APIs with cRPC HTTP router, Hono integration, webhooks, streaming, and client integration. Route builder basics → SKILL.md Section 9.
4
+
5
+ Prerequisites: `setup/server.md`.
6
+
7
+ ## Setup
8
+
9
+ ### Route Builders
10
+
11
+ ```ts
12
+ // convex/lib/crpc.ts
13
+ import { CRPCError, initCRPC } from "kitcn/server";
14
+
15
+ const c = initCRPC.dataModel<DataModel>().context({}).create({});
16
+
17
+ export const publicRoute = c.httpAction;
18
+
19
+ export const authRoute = c.httpAction.use(async ({ ctx, next }) => {
20
+ const identity = await ctx.auth.getUserIdentity();
21
+ if (!identity) throw new CRPCError({ code: "UNAUTHORIZED" });
22
+ return next({ ctx: { ...ctx, userId: identity.subject } });
23
+ });
24
+
25
+ export const router = c.router;
26
+ ```
27
+
28
+ ### HTTP Registration with Hono
29
+
30
+ Use `kitcn/auth/http` for auth route helpers; it auto-installs the Convex-safe `MessageChannel` polyfill.
31
+
32
+ ```ts
33
+ // convex/functions/http.ts
34
+ import { authMiddleware } from "kitcn/auth/http";
35
+ import { createHttpRouter } from "kitcn/server";
36
+ import { Hono } from "hono";
37
+ import { cors } from "hono/cors";
38
+ import { router } from "../lib/crpc";
39
+ import { getAuth } from "./generated/auth";
40
+ import { todosRouter } from "../routers/todos";
41
+ import { health } from "../routers/health";
42
+
43
+ const app = new Hono();
44
+
45
+ app.use(
46
+ "/api/*",
47
+ cors({
48
+ origin: process.env.SITE_URL!,
49
+ allowHeaders: ["Content-Type", "Authorization", "Better-Auth-Cookie"],
50
+ exposeHeaders: ["Set-Better-Auth-Cookie"],
51
+ credentials: true,
52
+ })
53
+ );
54
+
55
+ app.use(authMiddleware(getAuth));
56
+
57
+ export const httpRouter = router({
58
+ health,
59
+ todos: todosRouter,
60
+ });
61
+
62
+ export default createHttpRouter(app, httpRouter);
63
+ ```
64
+
65
+ | Component | Purpose |
66
+ | ----------------------------------- | ------------------------------------------ |
67
+ | `Hono` | Route handling, middleware, CORS |
68
+ | `authMiddleware(getAuth)` | Better Auth routes middleware |
69
+ | `createHttpRouter(app, httpRouter)` | Creates Convex HttpRouter with Hono + cRPC |
70
+
71
+ ## Defining Routes
72
+
73
+ ### GET with Search Params
74
+
75
+ ```ts
76
+ import { createTodosCaller } from "../functions/generated/todos.runtime";
77
+
78
+ export const list = publicRoute
79
+ .get("/api/todos")
80
+ .searchParams(
81
+ z.object({
82
+ limit: z.coerce.number().optional().default(10),
83
+ offset: z.coerce.number().optional().default(0),
84
+ })
85
+ )
86
+ .output(z.array(todoSchema))
87
+ .query(async ({ ctx, searchParams }) => {
88
+ const caller = createTodosCaller(ctx);
89
+ return caller.list({
90
+ limit: searchParams.limit,
91
+ offset: searchParams.offset,
92
+ });
93
+ });
94
+ ```
95
+
96
+ Use `z.coerce.number()` for search params since URL query strings are always strings.
97
+
98
+ ### GET with Path Params
99
+
100
+ ```ts
101
+ export const get = publicRoute
102
+ .get("/api/todos/:id")
103
+ .params(z.object({ id: z.string() }))
104
+ .output(todoSchema.nullable())
105
+ .query(async ({ ctx, params }) => {
106
+ const caller = createTodosCaller(ctx);
107
+ return caller.get({ id: params.id });
108
+ });
109
+ ```
110
+
111
+ ### POST / PATCH / DELETE
112
+
113
+ ```ts
114
+ export const create = authRoute
115
+ .post("/api/todos")
116
+ .input(
117
+ z.object({ title: z.string().min(1), description: z.string().optional() })
118
+ )
119
+ .output(z.object({ id: z.string() }))
120
+ .mutation(async ({ ctx, input }) => {
121
+ const caller = createTodoInternalCaller(ctx);
122
+ const id = await caller.create({ userId: ctx.userId, ...input });
123
+ return { id };
124
+ });
125
+
126
+ export const update = authRoute
127
+ .patch("/api/todos/:id")
128
+ .params(z.object({ id: z.string() }))
129
+ .input(
130
+ z.object({
131
+ title: z.string().optional(),
132
+ completed: z.boolean().optional(),
133
+ })
134
+ )
135
+ .output(z.object({ success: z.boolean() }))
136
+ .mutation(async ({ ctx, params, input }) => {
137
+ const caller = createTodoInternalCaller(ctx);
138
+ await caller.update({ id: params.id, ...input });
139
+ return { success: true };
140
+ });
141
+
142
+ export const deleteTodo = authRoute
143
+ .delete("/api/todos/:id")
144
+ .params(z.object({ id: z.string() }))
145
+ .output(z.object({ success: z.boolean() }))
146
+ .mutation(async ({ ctx, params }) => {
147
+ const caller = createTodoInternalCaller(ctx);
148
+ await caller.deleteTodo({ id: params.id });
149
+ return { success: true };
150
+ });
151
+ ```
152
+
153
+ ### Routers
154
+
155
+ ```ts
156
+ // convex/routers/todos.ts
157
+ export const todosRouter = router({
158
+ list,
159
+ get,
160
+ create,
161
+ update,
162
+ delete: deleteTodo,
163
+ });
164
+ ```
165
+
166
+ ### Combined Schemas
167
+
168
+ ```ts
169
+ export const createTask = authRoute
170
+ .post("/api/projects/:projectId/tasks")
171
+ .params(z.object({ projectId: z.string() }))
172
+ .searchParams(z.object({ notify: z.coerce.boolean().optional() }))
173
+ .input(z.object({ title: z.string(), description: z.string().optional() }))
174
+ .output(z.object({ taskId: z.string(), projectId: z.string() }))
175
+ .mutation(async ({ ctx, params, searchParams, input }) => {
176
+ const caller = createTasksCaller(ctx);
177
+ const taskId = await caller.create({
178
+ projectId: params.projectId,
179
+ ...input,
180
+ });
181
+ if (searchParams.notify) {
182
+ await caller.schedule.now.sendNotification({ taskId });
183
+ }
184
+ return { taskId, projectId: params.projectId };
185
+ });
186
+ ```
187
+
188
+ ## FormData Uploads
189
+
190
+ ```ts
191
+ // Server
192
+ export const upload = authRoute
193
+ .post("/api/files/upload")
194
+ .form(
195
+ z.object({
196
+ file: z.instanceof(File),
197
+ title: z.string().optional(),
198
+ tags: z.array(z.string()).optional(),
199
+ })
200
+ )
201
+ .mutation(async ({ ctx, c, form }) => {
202
+ const storageId = await ctx.storage.store(form.file);
203
+ return c.json({ storageId, filename: form.file.name });
204
+ });
205
+
206
+ // Client
207
+ uploadFile.mutate({
208
+ form: { file: selectedFile, title: "My Document", tags: ["work"] },
209
+ });
210
+ ```
211
+
212
+ ## Metadata & Middleware
213
+
214
+ ```ts
215
+ export const heavyEndpoint = publicRoute
216
+ .meta({ ratelimit: "api/heavy" })
217
+ .get("/api/reports")
218
+ .query(async ({ ctx }) => {
219
+ const caller = createReportsCaller(ctx);
220
+ return caller.generate({});
221
+ });
222
+
223
+ // Chained meta (shallow merge)
224
+ export const adminEndpoint = authRoute
225
+ .meta({ role: "admin" })
226
+ .meta({ ratelimit: "api/admin" })
227
+ .delete("/api/users/:id")
228
+ .params(z.object({ id: z.string() }))
229
+ .mutation(async ({ ctx, params }) => {
230
+ const caller = createAdminCaller(ctx);
231
+ await caller.deleteUser({ id: params.id });
232
+ });
233
+
234
+ // Custom middleware extending context
235
+ export const withPermissions = authRoute
236
+ .use(async ({ ctx, next }) => {
237
+ const caller = createPermissionsCaller(ctx);
238
+ const permissions = await caller.get({ userId: ctx.userId });
239
+ return next({ ctx: { ...ctx, permissions } });
240
+ })
241
+ .get("/api/protected")
242
+ .query(async ({ ctx }) => {
243
+ if (!ctx.permissions.includes("admin")) {
244
+ throw new CRPCError({ code: "FORBIDDEN", message: "Admin required" });
245
+ }
246
+ return { data: "secret" };
247
+ });
248
+ ```
249
+
250
+ ## Optional Auth
251
+
252
+ ```ts
253
+ export const publicOrAuth = optionalAuthRoute
254
+ .get("/api/content")
255
+ .query(async ({ ctx }) => {
256
+ const caller = createContentCaller(ctx);
257
+ const userId: Id<"user"> | null = ctx.userId;
258
+ if (userId) return caller.personalized({ userId });
259
+ return caller.public({});
260
+ });
261
+ ```
262
+
263
+ ## Error Handling
264
+
265
+ See [Error Codes](#error-codes) in API Reference. Zod validation failures auto-return `400 Bad Request` with error details.
266
+
267
+ ## Custom Responses
268
+
269
+ cRPC handlers receive `c` (Hono Context) for custom responses:
270
+
271
+ ```ts
272
+ // File download
273
+ export const download = authRoute
274
+ .get("/api/todos/export/:format")
275
+ .params(z.object({ format: z.enum(["json", "csv"]) }))
276
+ .query(async ({ ctx, params, c }) => {
277
+ const caller = createTodosCaller(ctx);
278
+ const todos = await caller.list({ limit: 100 });
279
+ c.header(
280
+ "Content-Disposition",
281
+ `attachment; filename="todos.${params.format}"`
282
+ );
283
+ c.header("Cache-Control", "no-cache");
284
+ if (params.format === "csv") {
285
+ const csv = [
286
+ "id,title,completed",
287
+ ...todos.map((t) => `${t.id},${t.title},${t.completed}`),
288
+ ].join("\n");
289
+ return c.text(csv);
290
+ }
291
+ return c.json({ todos });
292
+ });
293
+
294
+ // Redirect
295
+ export const redirect = publicRoute
296
+ .get("/api/old-path")
297
+ .query(async ({ c }) => c.redirect("/api/new-path", 301));
298
+ ```
299
+
300
+ | Method | Description |
301
+ | -------------------------- | -------------------- |
302
+ | `c.json(data)` | Return JSON response |
303
+ | `c.text(str)` | Return text response |
304
+ | `c.redirect(url, status?)` | Return redirect |
305
+ | `c.header(name, value)` | Set response header |
306
+ | `c.req.header(name)` | Get request header |
307
+ | `c.req.text()` | Get raw body as text |
308
+
309
+ ## Streaming
310
+
311
+ ### Server-Sent Events
312
+
313
+ ```ts
314
+ import { streamText } from "hono/streaming";
315
+
316
+ export const events = publicRoute
317
+ .get("/api/stream")
318
+ .query(async ({ ctx, c }) => {
319
+ c.header("Content-Type", "text/event-stream");
320
+ c.header("Cache-Control", "no-cache");
321
+ return streamText(c, async (stream) => {
322
+ for (let i = 0; i < 10; i++) {
323
+ const caller = createDataCaller(ctx);
324
+ const data = await caller.getChunk({ index: i });
325
+ await stream.write(`data: ${JSON.stringify(data)}\n\n`);
326
+ await stream.sleep(1000);
327
+ }
328
+ });
329
+ });
330
+ ```
331
+
332
+ ### AI Streaming
333
+
334
+ ```ts
335
+ import { stream } from "hono/streaming";
336
+
337
+ export const chat = publicRoute
338
+ .post("/api/ai/stream")
339
+ .input(z.object({ prompt: z.string() }))
340
+ .mutation(async ({ ctx, input, c }) => {
341
+ const aiCaller = createAiCaller(ctx);
342
+ c.header("Content-Type", "text/event-stream");
343
+ c.header("Cache-Control", "no-cache");
344
+ const aiStream = await aiCaller.actions.streamResponse({
345
+ prompt: input.prompt,
346
+ });
347
+ return stream(c, async (stream) => {
348
+ await stream.pipe(aiStream);
349
+ });
350
+ });
351
+ ```
352
+
353
+ ## Rate Limiting
354
+
355
+ ```ts
356
+ export const ratelimited = publicRoute
357
+ .post("/api/public")
358
+ .input(z.object({ data: z.string() }))
359
+ .mutation(async ({ ctx, input, c }) => {
360
+ const ip =
361
+ c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ??
362
+ c.req.header("CF-Connecting-IP") ??
363
+ "unknown";
364
+ const ratelimitCaller = createRatelimitCaller(ctx);
365
+ const allowed = await ratelimitCaller.check({
366
+ key: `http:${ip}`,
367
+ limit: 100,
368
+ window: 3600000,
369
+ });
370
+ if (!allowed)
371
+ return c.text("Rate limit exceeded", 429, { "Retry-After": "3600" });
372
+ const apiCaller = createApiCaller(ctx);
373
+ const result = await apiCaller.process({ data: input.data });
374
+ return c.json(result);
375
+ });
376
+ ```
377
+
378
+ ## Webhooks
379
+
380
+ ### Stripe
381
+
382
+ ```ts
383
+ export const stripeWebhook = publicRoute
384
+ .post("/webhooks/stripe")
385
+ .mutation(async ({ ctx, c }) => {
386
+ const stripeCaller = createStripeCaller(ctx);
387
+ const signature = c.req.header("stripe-signature");
388
+ if (!signature)
389
+ throw new CRPCError({ code: "BAD_REQUEST", message: "No signature" });
390
+
391
+ const body = await c.req.text();
392
+ const isValid = await stripeCaller.actions.verify({ body, signature });
393
+ if (!isValid)
394
+ throw new CRPCError({
395
+ code: "BAD_REQUEST",
396
+ message: "Invalid signature",
397
+ });
398
+
399
+ const event = JSON.parse(body);
400
+ switch (event.type) {
401
+ case "payment_intent.succeeded":
402
+ const paymentsCaller = createPaymentsCaller(ctx);
403
+ await paymentsCaller.markPaid({
404
+ paymentIntentId: event.data.object.id,
405
+ });
406
+ break;
407
+ case "customer.subscription.deleted":
408
+ const subscriptionsCaller = createSubscriptionsCaller(ctx);
409
+ await subscriptionsCaller.cancel({
410
+ subscriptionId: event.data.object.id,
411
+ });
412
+ break;
413
+ }
414
+ return c.text("OK", 200);
415
+ });
416
+ ```
417
+
418
+ ### Discord Bot
419
+
420
+ ```ts
421
+ import { verifyKey } from "discord-interactions";
422
+
423
+ export const discordWebhook = publicRoute
424
+ .post("/webhooks/discord")
425
+ .mutation(async ({ ctx, c }) => {
426
+ const signature = c.req.header("X-Signature-Ed25519");
427
+ const timestamp = c.req.header("X-Signature-Timestamp");
428
+ if (!signature || !timestamp)
429
+ throw new CRPCError({
430
+ code: "UNAUTHORIZED",
431
+ message: "Missing signature",
432
+ });
433
+
434
+ const body = await c.req.text();
435
+ if (
436
+ !verifyKey(body, signature, timestamp, process.env.DISCORD_PUBLIC_KEY!)
437
+ ) {
438
+ throw new CRPCError({
439
+ code: "UNAUTHORIZED",
440
+ message: "Invalid signature",
441
+ });
442
+ }
443
+
444
+ const interaction = JSON.parse(body);
445
+ if (interaction.type === 1) return c.json({ type: 1 }); // PING
446
+ if (interaction.type === 2) {
447
+ const statsCaller = createStatsCaller(ctx);
448
+ const discordCaller = createDiscordCaller(ctx);
449
+ switch (interaction.data.name) {
450
+ case "stats":
451
+ const stats = await statsCaller.get({});
452
+ return c.json({
453
+ type: 4,
454
+ data: { content: `Users: ${stats.users}, Posts: ${stats.posts}` },
455
+ });
456
+ case "create":
457
+ await discordCaller.schedule.now.processCreate({
458
+ token: interaction.token,
459
+ });
460
+ return c.json({ type: 5 }); // DEFERRED
461
+ default:
462
+ return c.json({ type: 4, data: { content: "Unknown command" } });
463
+ }
464
+ }
465
+ if (interaction.type === 3) {
466
+ const discordCaller = createDiscordCaller(ctx);
467
+ await discordCaller.handleButton({
468
+ customId: interaction.data.custom_id,
469
+ userId: interaction.user.id,
470
+ });
471
+ return c.json({ type: 7, data: { content: "Done!" } });
472
+ }
473
+ throw new CRPCError({
474
+ code: "BAD_REQUEST",
475
+ message: "Unknown interaction",
476
+ });
477
+ });
478
+ ```
479
+
480
+ ## React Client
481
+
482
+ See [Input Args](#input-args) in API Reference.
483
+
484
+ ### Query Patterns
485
+
486
+ ```ts
487
+ // GET with searchParams
488
+ crpc.http.todos.list.queryOptions({ searchParams: { limit: "10" } });
489
+
490
+ // GET with path params
491
+ crpc.http.todos.get.queryOptions({ params: { id: todoId } });
492
+
493
+ // GET with custom headers
494
+ crpc.http.todos.list.queryOptions({
495
+ searchParams: { limit: "10" },
496
+ headers: { "X-Custom": "value" },
497
+ });
498
+ ```
499
+
500
+ ### One-Time Fetch
501
+
502
+ ```ts
503
+ // For exports/downloads (no caching, mutation semantics)
504
+ const exportTodos = useMutation(crpc.http.todos.export.mutationOptions());
505
+ exportTodos.mutate({ params: { format: "csv" } });
506
+ ```
507
+
508
+ ### Vanilla Client
509
+
510
+ ```ts
511
+ const client = useCRPCClient();
512
+ const todos = await client.http.todos.list.query();
513
+ await client.http.todos.create.mutate({ title: "New todo" });
514
+
515
+ // For cache-aware fetches in render context
516
+ const queryClient = useQueryClient();
517
+ const todos = await queryClient.fetchQuery(crpc.http.todos.list.queryOptions());
518
+ ```
519
+
520
+ ### staticQueryOptions
521
+
522
+ For prefetching in event handlers (doesn't use hooks internally):
523
+
524
+ ```ts
525
+ const queryClient = useQueryClient();
526
+ const handleMouseEnter = () => {
527
+ queryClient.prefetchQuery(crpc.http.todos.list.staticQueryOptions());
528
+ };
529
+ ```
530
+
531
+ `staticQueryOptions` doesn't include reactive auth state. Auth handled at execution time.
532
+
533
+ ### Mutation Patterns
534
+
535
+ ```ts
536
+ const createTodo = useMutation(
537
+ crpc.http.todos.create.mutationOptions({ onSuccess: () => queryClient.invalidateQueries(...) })
538
+ );
539
+
540
+ createTodo.mutate({ title: 'New Todo' }); // JSON body at root
541
+ updateTodo.mutate({ params: { id: '123' }, completed: true }); // PATCH with params + body
542
+ deleteTodo.mutate({ params: { id: '123' } }); // DELETE with params
543
+ uploadFile.mutate({ form: { file: selectedFile, description: 'My file' } }); // FormData
544
+ ```
545
+
546
+ ### Cache Invalidation
547
+
548
+ ```ts
549
+ const updateTodo = useMutation(
550
+ crpc.http.todos.update.mutationOptions({
551
+ onSuccess: (_, vars) => {
552
+ queryClient.invalidateQueries(crpc.http.todos.list.queryFilter());
553
+ queryClient.invalidateQueries(
554
+ crpc.http.todos.get.queryFilter({ params: { id: vars.params?.id } })
555
+ );
556
+ },
557
+ })
558
+ );
559
+ ```
560
+
561
+ See [Client Methods](#client-methods) in API Reference.
562
+
563
+ ## RSC Prefetching
564
+
565
+ ```tsx
566
+ // app/todos/page.tsx
567
+ import { crpc, HydrateClient, prefetch } from "@/lib/convex/rsc";
568
+
569
+ export default async function TodosPage() {
570
+ prefetch(
571
+ crpc.http.todos.list.queryOptions({ searchParams: { limit: "10" } })
572
+ );
573
+ return (
574
+ <HydrateClient>
575
+ <TodoList />
576
+ </HydrateClient>
577
+ );
578
+ }
579
+ ```
580
+
581
+ ### Awaited Prefetch
582
+
583
+ ```tsx
584
+ const todo = await preloadQuery(
585
+ crpc.http.todos.get.queryOptions({ params: { id } })
586
+ );
587
+ if (!todo) notFound();
588
+ ```
589
+
590
+ ### Auth-Aware Prefetch
591
+
592
+ ```tsx
593
+ prefetch(
594
+ crpc.http.todos.list.queryOptions(
595
+ { searchParams: { limit: "10" } },
596
+ { skipUnauth: true }
597
+ )
598
+ );
599
+ ```
600
+
601
+ | Pattern | Blocking | Server Access | Client Hydration |
602
+ | ---------------- | -------- | ------------- | ---------------- |
603
+ | `prefetch()` | No | No | Yes |
604
+ | `preloadQuery()` | Yes | Yes | Yes |
605
+
606
+ ## Server-Side Calls
607
+
608
+ ```ts
609
+ import { createContext } from "@/lib/convex/server";
610
+
611
+ const ctx = await createContext({ headers: request.headers });
612
+ const todos = await ctx.caller.todos.list({ limit: 10 });
613
+ if (ctx.isAuthenticated) await ctx.caller.todos.create({ title: "New task" });
614
+ ```
615
+
616
+ ## API Reference
617
+
618
+ ### Route Builder Patterns
619
+
620
+ | Pattern | Use Case |
621
+ | ---------------------------------------- | ------------------------ |
622
+ | `publicRoute.get('/path').query()` | Public GET endpoint |
623
+ | `authRoute.post('/path').mutation()` | Auth-required POST |
624
+ | `optionalAuthRoute.get('/path').query()` | Optional auth endpoint |
625
+ | `.params(z.object({id}))` | Path params `/todos/:id` |
626
+ | `.searchParams(z.object({limit}))` | Query params `?limit=10` |
627
+ | `.input(z.object({...}))` | JSON body (POST/PATCH) |
628
+ | `.form(z.object({file, description}))` | FormData uploads |
629
+ | `.output(z.object({...}))` | Response validation |
630
+ | `.meta({ ratelimit: 'api/heavy' })` | Procedure metadata |
631
+ | `.use(middleware)` | Custom middleware |
632
+ | `router({ endpoint1, endpoint2 })` | Group endpoints |
633
+
634
+ ### HTTP Methods
635
+
636
+ | Method | Builder | Use Case | Has Body |
637
+ | ------ | ---------------------- | ----------------- | -------- |
638
+ | GET | `.get().query()` | Read operations | No |
639
+ | POST | `.post().mutation()` | Create operations | Yes |
640
+ | PATCH | `.patch().mutation()` | Partial updates | Yes |
641
+ | DELETE | `.delete().mutation()` | Delete operations | No |
642
+
643
+ ### Error Codes
644
+
645
+ | Code | HTTP Status | Use Case |
646
+ | ----------------------- | ----------- | --------------------------------- |
647
+ | `BAD_REQUEST` | 400 | Invalid request format |
648
+ | `UNAUTHORIZED` | 401 | Missing or invalid authentication |
649
+ | `FORBIDDEN` | 403 | Authenticated but not authorized |
650
+ | `NOT_FOUND` | 404 | Resource doesn't exist |
651
+ | `CONFLICT` | 409 | Resource conflict (duplicate) |
652
+ | `UNPROCESSABLE_CONTENT` | 422 | Validation failed |
653
+ | `TOO_MANY_REQUESTS` | 429 | Rate limit exceeded |
654
+ | `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error |
655
+
656
+ ### Input Args
657
+
658
+ | Property | Type | Description |
659
+ | -------------- | --------------------------------------- | ------------------------------------- |
660
+ | `params` | `Record<string, string>` | Path parameters (`:id`) |
661
+ | `searchParams` | `Record<string, string \| string[]>` | Query string params |
662
+ | `form` | `z.infer<TForm>` | Typed FormData (if `.form()` defined) |
663
+ | `fetch` | `typeof fetch` | Custom fetch function |
664
+ | `init` | `RequestInit` | Request options |
665
+ | `headers` | `Record<string, string> \| (() => ...)` | Headers (incl. cookies) |
666
+ | `[key]` | `unknown` | JSON body fields at root |
667
+
668
+ ### Client Methods
669
+
670
+ | Method | Signature | Description |
671
+ | -------------------- | --------------------- | ----------------------------------------- |
672
+ | `queryOptions` | `(args?, queryOpts?)` | Options for `useQuery`/`useSuspenseQuery` |
673
+ | `staticQueryOptions` | `(args?, queryOpts?)` | For event handlers/prefetching (no hooks) |
674
+ | `mutationOptions` | `(mutationOpts?)` | Options for `useMutation` |
675
+ | `queryKey` | `(args?)` | Get query key for cache operations |
676
+ | `queryFilter` | `(args?, filters?)` | Filter for `invalidateQueries` |