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,657 @@
1
+ # React & RSC Reference
2
+
3
+ > Prerequisites: `setup/react.md`, `setup/next.md`
4
+
5
+ Covers all kitcn React client, TanStack Query integration, and Next.js RSC patterns. Assumes TanStack Query baseline knowledge.
6
+
7
+ ## Setup
8
+
9
+ ### createCRPCContext
10
+
11
+ ```ts
12
+ // src/lib/convex/crpc.tsx
13
+ import { api } from '@convex/api';
14
+ import { createCRPCContext } from 'kitcn/react';
15
+
16
+ export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
17
+ api,
18
+ convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
19
+ transformer, // optional — Date always enabled ($date wire tag). Use createTaggedTransformer for extra codecs.
20
+ });
21
+ ```
22
+
23
+ | Export | Description |
24
+ |--------|-------------|
25
+ | `CRPCProvider` | Context provider — wraps children with cRPC proxy |
26
+ | `useCRPC` | Hook → cRPC proxy for `queryOptions`/`mutationOptions`/`infiniteQueryOptions` |
27
+ | `useCRPCClient` | Hook → typed vanilla client for imperative `client.path.query()`/`mutate()` |
28
+
29
+ ### QueryClient
30
+
31
+ cRPC auto-sets `staleTime: Infinity`, `refetch*: false` per query (Convex pushes via WebSocket — never stale).
32
+
33
+ ```ts
34
+ // src/lib/convex/query-client.ts
35
+ import { defaultShouldDehydrateQuery, QueryCache, QueryClient } from '@tanstack/react-query';
36
+ import { isCRPCClientError, isCRPCError } from 'kitcn/crpc';
37
+ import SuperJSON from 'superjson';
38
+
39
+ // Shared hydration config for SSR (client + server)
40
+ export const hydrationConfig = {
41
+ dehydrate: {
42
+ serializeData: SuperJSON.serialize,
43
+ shouldDehydrateQuery: (query) =>
44
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
45
+ shouldRedactErrors: () => false,
46
+ },
47
+ hydrate: { deserializeData: SuperJSON.deserialize },
48
+ };
49
+
50
+ export function createQueryClient() {
51
+ return new QueryClient({
52
+ queryCache: new QueryCache({
53
+ onError: (error) => {
54
+ if (isCRPCClientError(error)) {
55
+ console.log(`[CRPC] ${error.code}:`, error.functionName);
56
+ }
57
+ },
58
+ }),
59
+ defaultOptions: {
60
+ ...hydrationConfig,
61
+ mutations: {
62
+ onError: (err) => {
63
+ const error = err as Error & { data?: { message?: string } };
64
+ toast.error(error.data?.message || error.message);
65
+ },
66
+ },
67
+ queries: {
68
+ retry: (failureCount, error) => {
69
+ if (isCRPCError(error)) return false; // don't retry deterministic errors
70
+ return failureCount < 3;
71
+ },
72
+ retryDelay: (i) => Math.min(2000 * 2 ** i, 30_000),
73
+ },
74
+ },
75
+ });
76
+ }
77
+ ```
78
+
79
+ ### Provider Hierarchy
80
+
81
+ **Without auth:**
82
+ ```tsx
83
+ // src/lib/convex/convex-provider.tsx
84
+ 'use client';
85
+ import { QueryClientProvider } from '@tanstack/react-query';
86
+ import { ConvexProvider, ConvexReactClient, getQueryClientSingleton, getConvexQueryClientSingleton } from 'kitcn/react';
87
+ import { CRPCProvider } from '@/lib/convex/crpc';
88
+ import { createQueryClient } from '@/lib/convex/query-client';
89
+
90
+ const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
91
+
92
+ export function AppConvexProvider({ children }) {
93
+ return (
94
+ <ConvexProvider client={convex}>
95
+ <QueryProvider>{children}</QueryProvider>
96
+ </ConvexProvider>
97
+ );
98
+ }
99
+
100
+ function QueryProvider({ children }) {
101
+ const queryClient = getQueryClientSingleton(createQueryClient);
102
+ const convexQueryClient = getConvexQueryClientSingleton({ convex, queryClient });
103
+ return (
104
+ <QueryClientProvider client={queryClient}>
105
+ <CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
106
+ {children}
107
+ </CRPCProvider>
108
+ </QueryClientProvider>
109
+ );
110
+ }
111
+ ```
112
+
113
+ **With auth** — swap `ConvexProvider` for `ConvexAuthProvider`:
114
+ ```tsx
115
+ import { ConvexAuthProvider } from 'kitcn/auth/client';
116
+ import { ConvexReactClient, getConvexQueryClientSingleton, getQueryClientSingleton, useAuthStore } from 'kitcn/react';
117
+
118
+ const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
119
+
120
+ export function AppConvexProvider({ children, token }: { children: ReactNode; token?: string }) {
121
+ const router = useRouter();
122
+ return (
123
+ <ConvexAuthProvider
124
+ authClient={authClient}
125
+ client={convex}
126
+ initialToken={token}
127
+ onMutationUnauthorized={() => router.push('/login')}
128
+ onQueryUnauthorized={() => router.push('/login')}
129
+ >
130
+ <QueryProvider>{children}</QueryProvider>
131
+ </ConvexAuthProvider>
132
+ );
133
+ }
134
+
135
+ function QueryProvider({ children }) {
136
+ const authStore = useAuthStore(); // pass to singleton
137
+ const queryClient = getQueryClientSingleton(createQueryClient);
138
+ const convexQueryClient = getConvexQueryClientSingleton({ authStore, convex, queryClient });
139
+ return (
140
+ <QueryClientProvider client={queryClient}>
141
+ <CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
142
+ {children}
143
+ </CRPCProvider>
144
+ </QueryClientProvider>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ### Singleton Helpers
150
+
151
+ | Helper | Behavior |
152
+ |--------|----------|
153
+ | `getQueryClientSingleton(factory)` | Same instance on client, fresh per SSR request |
154
+ | `getConvexQueryClientSingleton(opts)` | Creates/connects ConvexQueryClient bridge |
155
+
156
+ `getConvexQueryClientSingleton` options:
157
+ - `convex` — ConvexReactClient
158
+ - `queryClient` — TanStack QueryClient
159
+ - `authStore` — from `useAuthStore()` (auth apps only)
160
+ - `unsubscribeDelay` — ms before unsubscribing after unmount (default 3000). Covers StrictMode + quick back-nav.
161
+
162
+ ### ConvexQueryClient (Bridge)
163
+
164
+ Bridges WebSocket subscriptions → TanStack Query cache. Push model (not pull):
165
+
166
+ ```
167
+ useQuery() → WebSocket subscription → real-time updates → cache always fresh
168
+ ```
169
+
170
+ Defaults: `staleTime: Infinity`, `gcTime: 5min`, `refetchOnMount: false`, `refetchOnWindowFocus: false`.
171
+
172
+ Lifecycle: Mount → subscribe → unmount → wait `unsubscribeDelay` → unsubscribe (cache persists for `gcTime`).
173
+
174
+ ---
175
+
176
+ ## Queries
177
+
178
+ ### queryOptions
179
+
180
+ ```ts
181
+ const crpc = useCRPC();
182
+ const { data } = useQuery(crpc.user.list.queryOptions({}));
183
+ const { data } = useQuery(crpc.user.get.queryOptions({ id }));
184
+ const { data } = useQuery(crpc.user.get.queryOptions({ id }, { enabled: !!id, placeholderData: null }));
185
+ ```
186
+
187
+ Signature: `crpc.path.queryOptions(args, options?)`
188
+
189
+ cRPC-specific options beyond standard TanStack Query:
190
+
191
+ | Option | Type | Default | Description |
192
+ |--------|------|---------|-------------|
193
+ | `skipUnauth` | `boolean` | `false` | Skip query when not authenticated (returns undefined) |
194
+ | `subscribe` | `boolean` | `true` | Enable real-time WebSocket subscription |
195
+
196
+ **With `select`** — spread options, add select separately:
197
+ ```ts
198
+ const { data } = useSuspenseQuery({
199
+ ...crpc.http.health.queryOptions(),
200
+ select: (data) => data.status, // data: string
201
+ });
202
+ ```
203
+
204
+ ### Real-time Subscriptions
205
+
206
+ Default ON. Every `queryOptions` subscribes to Convex WebSocket. Disable with `subscribe: false`:
207
+ ```ts
208
+ useQuery(crpc.analytics.getReport.queryOptions({ period }, { subscribe: false }));
209
+ // refresh manually:
210
+ queryClient.invalidateQueries(crpc.analytics.getReport.queryFilter());
211
+ ```
212
+
213
+ ### Auth-Aware Queries
214
+
215
+ **`skipUnauth`** — client-side: returns undefined when not authenticated:
216
+ ```ts
217
+ useQuery(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
218
+ ```
219
+
220
+ **`meta({ auth })` on procedures** — controls query behavior during auth loading:
221
+
222
+ | Procedure type | Auth loading | Logged out |
223
+ |----------------|-------------|------------|
224
+ | `publicQuery` | Runs immediately | Runs |
225
+ | `optionalAuthQuery` (auth: 'optional') | **Waits** | Runs |
226
+ | `authQuery` (auth: 'required') | **Waits** | **Skips** |
227
+
228
+ The procedure builders (`authQuery`, `publicQuery`, etc.) already include correct `.meta()` settings.
229
+
230
+ ### Conditional Queries
231
+
232
+ ```ts
233
+ // enabled
234
+ useQuery(crpc.user.getSettings.queryOptions({ userId: user?.id }, { enabled: !!user }));
235
+
236
+ // skipToken
237
+ import { skipToken } from '@tanstack/react-query';
238
+ useQuery(crpc.user.get.queryOptions(userId ? { id: userId } : skipToken));
239
+ ```
240
+
241
+ ### Query Keys & Filters
242
+
243
+ ```ts
244
+ const queryKey = crpc.user.list.queryKey({}); // ['convexQuery', 'user:list', {}]
245
+ const data = queryClient.getQueryData(queryKey);
246
+
247
+ const filter = crpc.user.list.queryFilter({}, { predicate: (q) => q.state.dataUpdatedAt > Date.now() - 60000 });
248
+ queryClient.invalidateQueries(filter);
249
+ ```
250
+
251
+ ### Imperative Calls
252
+
253
+ Three methods:
254
+
255
+ | Method | Context | Caching | Use Case |
256
+ |--------|---------|---------|----------|
257
+ | `client.*.query()` | Anywhere | None | Direct calls, no cache |
258
+ | `crpc.*.queryOptions()` | Render only | Cache | Components (uses hooks) |
259
+ | `crpc.*.staticQueryOptions()` | Anywhere | Cache | Prefetch, event handlers |
260
+
261
+ ```ts
262
+ // useCRPCClient — direct calls
263
+ const client = useCRPCClient();
264
+ const user = await client.user.get.query({ id });
265
+ await client.user.update.mutate({ id, name: 'test' });
266
+
267
+ // staticQueryOptions — prefetch in event handlers (no hooks, no reactive auth)
268
+ const handleMouseEnter = () => {
269
+ queryClient.prefetchQuery(crpc.user.get.staticQueryOptions({ id }));
270
+ };
271
+ ```
272
+
273
+ ### Actions as Queries
274
+
275
+ Actions (external API calls) auto-detected, no subscription:
276
+ ```ts
277
+ const { data } = useQuery(crpc.ai.analyze.queryOptions({ documentId }));
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Mutations
283
+
284
+ ### mutationOptions
285
+
286
+ ```ts
287
+ const crpc = useCRPC();
288
+ const mutation = useMutation(crpc.user.create.mutationOptions());
289
+ const mutation = useMutation(crpc.user.update.mutationOptions({
290
+ onSuccess: (data) => toast.success('Updated'),
291
+ onError: (error) => toast.error(error.data?.message ?? 'Failed'),
292
+ }));
293
+ ```
294
+
295
+ Signature: `crpc.path.mutationOptions(options?)` — standard TanStack mutation options except `mutationFn`.
296
+
297
+ ### Mutation Keys
298
+
299
+ ```ts
300
+ const key = crpc.user.create.mutationKey(); // ['convexMutation', 'user:create']
301
+ ```
302
+
303
+ ### Common Patterns
304
+
305
+ **Toast promise:**
306
+ ```ts
307
+ toast.promise(mutation.mutateAsync({ title }), {
308
+ loading: 'Creating...', success: 'Created!',
309
+ error: (e) => e.data?.message ?? 'Failed',
310
+ });
311
+ ```
312
+
313
+ **Form with cleanup:**
314
+ ```ts
315
+ const mutation = useMutation(crpc.user.update.mutationOptions({
316
+ onSuccess: () => { form.reset(); closeModal(); toast.success('Updated'); },
317
+ }));
318
+ ```
319
+
320
+ **Inline callbacks:**
321
+ ```ts
322
+ mutation.mutate({ id }, {
323
+ onSuccess: () => router.push('/sessions'),
324
+ onError: () => toast.error('Delete failed'),
325
+ });
326
+ ```
327
+
328
+ ### Actions as Mutations
329
+
330
+ Actions work with `mutationOptions` for external API calls (no real-time):
331
+ ```ts
332
+ const scrape = useMutation(crpc.scraper.scrapeLink.mutationOptions());
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Infinite Queries
338
+
339
+ Import `useInfiniteQuery` from `kitcn/react` (wraps TanStack with Convex subscription logic):
340
+
341
+ ```ts
342
+ import { useInfiniteQuery } from 'kitcn/react';
343
+
344
+ const crpc = useCRPC();
345
+ const { data, fetchNextPage, hasNextPage, isLoading, status } = useInfiniteQuery(
346
+ crpc.session.list.infiniteQueryOptions({ userId })
347
+ );
348
+ // data is flattened T[] — all loaded items
349
+ // status: 'LoadingFirstPage' | 'LoadingMore' | 'CanLoadMore' | 'Exhausted'
350
+ ```
351
+
352
+ ### infiniteQueryOptions
353
+
354
+ ```ts
355
+ crpc.path.infiniteQueryOptions(args, options?)
356
+ ```
357
+
358
+ | Option | Type | Description |
359
+ |--------|------|-------------|
360
+ | `limit` | `number` | Items per page (optional if `.paginated(limit)` on server, must be ≤ server limit) |
361
+ | `skipUnauth` | `boolean` | Skip when unauthenticated |
362
+
363
+ Access server limit: `crpc.session.list.meta.limit`
364
+
365
+ ### Backend Setup
366
+
367
+ ```ts
368
+ // convex/functions/session.ts
369
+ export const list = publicQuery
370
+ .input(z.object({ userId: z.string().optional() }))
371
+ .paginated({ limit: 20, item: SessionSchema })
372
+ .query(async ({ ctx, input }) => {
373
+ // input.cursor and input.limit auto-added
374
+ return ctx.orm.query.session.findMany({
375
+ where: input.userId ? { userId: input.userId } : undefined,
376
+ orderBy: { createdAt: 'desc' },
377
+ cursor: input.cursor,
378
+ limit: input.limit,
379
+ });
380
+ // output auto-wrapped as { continueCursor, isDone, page }
381
+ });
382
+ ```
383
+
384
+ `.paginated({ limit, item })`:
385
+ - Adds `cursor` (string|null) and `limit` (number) to input
386
+ - Auto-sets output schema: `{ continueCursor: string, isDone: boolean, page: T[] }`
387
+ - Must be called before `.query()`
388
+
389
+ ### Return Value
390
+
391
+ See [Infinite Query Return Value](#infinite-query-return-value) in the API Reference below.
392
+
393
+ ### Prefetching
394
+
395
+ ```ts
396
+ await queryClient.prefetchQuery(crpc.session.list.infiniteQueryOptions({ userId }));
397
+ ```
398
+
399
+ ### Placeholder Data
400
+
401
+ ```ts
402
+ const { data, isPlaceholderData } = useInfiniteQuery(
403
+ crpc.session.list.infiniteQueryOptions({}, {
404
+ placeholderData: Array.from({ length: crpc.session.list.meta.limit }).map((_, i) => ({
405
+ id: i.toString() as Id<'session'>, token: 'Loading...', expiresAt: 0,
406
+ })),
407
+ })
408
+ );
409
+ ```
410
+
411
+ ### Real-time & Error Recovery
412
+
413
+ Each page maintains its own WebSocket subscription. Auto-recovers on `InvalidCursor` (resets to page 0) and `splitCursor` (auto-splits page). Pagination state persists in `queryClient` for scroll restoration.
414
+
415
+ ---
416
+
417
+ ## Error Handling
418
+
419
+ ### Server Errors
420
+
421
+ `CRPCError` thrown server-side → arrives as `ConvexError` on client. Access via `error.data`:
422
+
423
+ ```ts
424
+ // Server: throw new CRPCError({ code: 'NOT_FOUND', message: 'Post not found' });
425
+ // Client:
426
+ const { error, isError } = useQuery(crpc.posts.get.queryOptions({ id }));
427
+ if (isError) toast.error(error.data?.message ?? 'Something went wrong');
428
+
429
+ // Mutation callback:
430
+ crpc.posts.create.mutationOptions({ onError: (error) => toast.error(error.data?.message ?? 'Failed') });
431
+
432
+ // Try/catch:
433
+ const error = err as Error & { data?: { message?: string } };
434
+ ```
435
+
436
+ ### Client Errors
437
+
438
+ `CRPCClientError` — thrown client-side when queries are skipped (auth):
439
+
440
+ ```ts
441
+ import { CRPCClientError, isCRPCClientError, isCRPCErrorCode } from 'kitcn/crpc';
442
+
443
+ if (isCRPCClientError(error)) {
444
+ error.code; // 'UNAUTHORIZED'
445
+ error.functionName; // 'user:getSettings'
446
+ }
447
+ if (isCRPCErrorCode(error, 'UNAUTHORIZED')) router.push('/login');
448
+ ```
449
+
450
+ | Code | Description |
451
+ |------|-------------|
452
+ | `UNAUTHORIZED` | Missing authentication |
453
+ | `FORBIDDEN` | Not authorized |
454
+ | `NOT_FOUND` | Resource not found |
455
+ | `BAD_REQUEST` | Invalid input |
456
+ | `TOO_MANY_REQUESTS` | Rate limited |
457
+
458
+ ### Global Error Handling
459
+
460
+ ```ts
461
+ new QueryClient({
462
+ queryCache: new QueryCache({
463
+ onError: (error) => {
464
+ if (isCRPCClientError(error)) console.log(`[CRPC] ${error.code}:`, error.functionName);
465
+ },
466
+ }),
467
+ });
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Type Inference
473
+
474
+ ```ts
475
+ import type { Api, ApiInputs, ApiOutputs } from '@convex/api';
476
+ ```
477
+
478
+ Bracket notation:
479
+ ```ts
480
+ type User = ApiOutputs['user']['get'];
481
+ type GetUserArgs = ApiInputs['user']['get'];
482
+ type OrgMember = ApiOutputs['organization']['members']['list'][number]; // array item
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Next.js Setup
488
+
489
+ ### Caller Factory
490
+
491
+ ```ts
492
+ // src/lib/convex/server.ts
493
+ import { api } from '@convex/api';
494
+ import { convexBetterAuth } from 'kitcn/auth/nextjs';
495
+
496
+ export const { createContext, createCaller, handler } = convexBetterAuth({
497
+ api,
498
+ convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
499
+ });
500
+ ```
501
+
502
+ | Export | Description |
503
+ |--------|-------------|
504
+ | `createContext` | RSC context with auth |
505
+ | `createCaller` | Server-side caller factory |
506
+ | `handler` | Next.js API route handler (`export const { GET, POST } = handler;`) |
507
+
508
+ Options: `api`, `convexSiteUrl`, `auth.jwtCache` (default true), `auth.isUnauthorized`.
509
+
510
+ ### Client Provider with Auth
511
+
512
+ ```tsx
513
+ // layout.tsx
514
+ const token = await caller.getToken();
515
+ return <ConvexProvider token={token}>{children}</ConvexProvider>;
516
+ ```
517
+
518
+ ### API Route
519
+
520
+ ```ts
521
+ // src/app/api/auth/[...all]/route.ts
522
+ import { handler } from '@/lib/convex/server';
523
+ export const { GET, POST } = handler;
524
+ ```
525
+
526
+ ---
527
+
528
+ ## RSC Patterns
529
+
530
+ ### RSC Setup
531
+
532
+ ```tsx
533
+ // src/lib/convex/rsc.tsx
534
+ import 'server-only';
535
+ import { createServerCRPCProxy, getServerQueryClientOptions } from 'kitcn/rsc';
536
+ import { cache } from 'react';
537
+ import { headers } from 'next/headers';
538
+ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
539
+ import { hydrationConfig } from './query-client';
540
+ import { createCaller, createContext } from './server';
541
+
542
+ const createRSCContext = cache(async () => createContext({ headers: await headers() }));
543
+
544
+ // Direct server calls (not cached/hydrated)
545
+ export const caller = createCaller(createRSCContext);
546
+
547
+ // Server cRPC proxy (queryOptions only, no mutations)
548
+ export const crpc = createServerCRPCProxy({ api });
549
+
550
+ // Server QueryClient with HTTP-based fetching
551
+ const createServerQueryClient = () => new QueryClient({
552
+ defaultOptions: {
553
+ ...hydrationConfig,
554
+ ...getServerQueryClientOptions({
555
+ getToken: caller.getToken,
556
+ convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
557
+ }),
558
+ },
559
+ });
560
+ export const getQueryClient = cache(createServerQueryClient);
561
+
562
+ // Fire-and-forget prefetch
563
+ export function prefetch<T extends { queryKey: readonly unknown[] }>(opts: T): void {
564
+ void getQueryClient().prefetchQuery(opts);
565
+ }
566
+
567
+ // Hydration wrapper
568
+ export function HydrateClient({ children }: { children: React.ReactNode }) {
569
+ return (
570
+ <HydrationBoundary state={dehydrate(getQueryClient())}>
571
+ {children}
572
+ </HydrationBoundary>
573
+ );
574
+ }
575
+
576
+ // Awaited fetch + hydration (equivalent to Convex preloadQuery)
577
+ export function preloadQuery<T>(options: FetchQueryOptions<T>): Promise<T> {
578
+ return getQueryClient().fetchQuery(options);
579
+ }
580
+ ```
581
+
582
+ ### Three RSC Patterns
583
+
584
+ | Pattern | Blocking | Returns data | Client hydration | Use case |
585
+ |---------|----------|-------------|------------------|----------|
586
+ | `prefetch` | No | No (void) | Yes | Client-only data, non-blocking |
587
+ | `caller` | Yes | Yes | **No** | Server-only logic (redirects, auth checks, sensitive data) |
588
+ | `preloadQuery` | Yes | Yes | Yes | Server + client data (metadata, 404 checks) |
589
+
590
+ **prefetch** (preferred — non-blocking, client owns data):
591
+ ```tsx
592
+ export default async function PostsPage() {
593
+ prefetch(crpc.posts.list.queryOptions({}));
594
+ return <HydrateClient><PostList /></HydrateClient>;
595
+ }
596
+ ```
597
+
598
+ **caller** (server-only, not hydrated):
599
+ ```tsx
600
+ const user = await caller.user.getSessionUser({});
601
+ if (!user?.isAdmin) redirect('/');
602
+ ```
603
+
604
+ **preloadQuery** (awaited + hydrated — use sparingly):
605
+ ```tsx
606
+ const post = await preloadQuery(crpc.posts.get.queryOptions({ id }));
607
+ if (!post) notFound();
608
+ return <HydrateClient><h1>{post.title}</h1><PostContent /></HydrateClient>;
609
+ ```
610
+
611
+ ### Auth-Aware Prefetching
612
+
613
+ ```tsx
614
+ prefetch(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
615
+ ```
616
+
617
+ ### Multiple Prefetches
618
+
619
+ ```tsx
620
+ prefetch(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
621
+ prefetch(crpc.posts.list.queryOptions({}));
622
+ prefetch(crpc.stats.dashboard.queryOptions({}));
623
+ return <HydrateClient><Dashboard /></HydrateClient>;
624
+ ```
625
+
626
+ ### Metadata Generation
627
+
628
+ ```tsx
629
+ export async function generateMetadata({ params }) {
630
+ const { id } = await params;
631
+ const post = await preloadQuery(crpc.posts.get.queryOptions({ id }));
632
+ return { title: post?.title ?? 'Not Found', description: post?.excerpt };
633
+ }
634
+ ```
635
+
636
+ ### HydrateClient Placement
637
+
638
+ Must wrap ALL client components that use prefetched queries. Server and client proxies generate identical query keys (`['convexQuery', funcRef, args]`).
639
+
640
+ ### Data Ownership Caveat
641
+
642
+ Don't render `preloadQuery` data in BOTH Server and Client components — the server-rendered part can't be revalidated by React Query. Prefer `prefetch` (let client own data) unless you need server-side access (metadata, 404, redirects).
643
+
644
+ ---
645
+
646
+ ## API Reference
647
+
648
+ ### Infinite Query Return Value
649
+
650
+ | Property | Type | Description |
651
+ |----------|------|-------------|
652
+ | `data` | `T[]` | Flattened array of all items |
653
+ | `pages` | `T[][]` | Raw page arrays |
654
+ | `fetchNextPage` | `(limit?) => void` | Load next page |
655
+ | `hasNextPage` | `boolean` | More pages exist |
656
+ | `status` | `PaginationStatus` | `'LoadingFirstPage' \| 'LoadingMore' \| 'CanLoadMore' \| 'Exhausted'` |
657
+ | `isPlaceholderData` | `boolean` | Showing placeholder |