kitcn 0.0.1 → 0.12.0

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 +13255 -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 -35
  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 +759 -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,470 @@
1
+ # Auth Core Reference
2
+
3
+ > Prerequisites: `setup/auth.md`
4
+
5
+ Covers Better Auth integration with Convex: server setup, client hooks, triggers, and auth flow. Assumes Better Auth baseline knowledge.
6
+
7
+ ## Key Concepts
8
+
9
+ **Local approach** — auth tables live in your app schema (not a component). Triggers directly access app tables via `ctx.orm`. Single transaction.
10
+
11
+ **Context-aware adapter** — generated `getAuth(ctx)` auto-selects:
12
+
13
+ | Context | Adapter | Behavior |
14
+ |---------|---------|----------|
15
+ | Query/Mutation (`ctx.db`) | Direct DB | No `runQuery`/`runMutation` wrapper |
16
+ | Action/HTTP | HTTP adapter | Uses `ctx.run*` APIs |
17
+
18
+ **Entrypoint**: `getAuth(ctx)` everywhere (query, mutation, action, HTTP).
19
+
20
+ ## Auth Flow
21
+
22
+ Two-step validation for every request (SSR or WebSocket):
23
+
24
+ 1. **JWT validation** (cryptographic) — decode, verify signature via JWKS, check `exp`
25
+ 2. **Session lookup** (database) — `session.id = sessionId AND expiresAt > now`
26
+
27
+ JWT validity doesn't guarantee access. Session lookup is the source of truth — deleting the session immediately invalidates access.
28
+
29
+ | Component | Storage | Invalidatable | Default Lifetime |
30
+ |-----------|---------|---------------|------------------|
31
+ | JWT | Cookie (signed) | No (stateless) | 15 min |
32
+ | Session | Convex DB | Yes (stateful) | 30 days |
33
+
34
+ ### SSR vs Client
35
+
36
+ | | SSR (HTTP) | Client (WebSocket) |
37
+ |---|---|---|
38
+ | Transport | HTTP per query | Persistent connection |
39
+ | Token source | Cookie / fetch from `/api/auth/convex/token` | WebSocket handshake |
40
+ | Validation | Per request | Once at connection, then cached |
41
+ | JWKS impact | +100-400ms per request (if dynamic) | +100-400ms blocking handshake (if dynamic) |
42
+
43
+ **Static JWKS** (recommended): instant validation. **Dynamic JWKS**: +100-400ms network calls.
44
+
45
+ ### Auth States
46
+
47
+ | Scenario | JWT | Session | Result |
48
+ |----------|-----|---------|--------|
49
+ | Normal | Valid | Valid | 200 OK |
50
+ | Sign out | Deleted | Deleted | 401 |
51
+ | Admin revokes session | Valid | Deleted | 401 on next request |
52
+ | JWT expired, session valid | Expired | Valid | Auto-refresh → 200 |
53
+ | JWT expired, session expired | Expired | Expired | 401 |
54
+ | User banned | Valid | Valid (banned) | 403 |
55
+
56
+ Client auto-refreshes expired JWTs with 60s leeway.
57
+
58
+ ---
59
+
60
+ ## Server Setup
61
+
62
+ Below is the reference for auth patterns.
63
+
64
+ ### 1. Install auth with CLI
65
+
66
+ Use the CLI-first path:
67
+
68
+ ```bash
69
+ npx kitcn add auth --yes
70
+ ```
71
+
72
+ If kitcn is not bootstrapped yet, start with `npx kitcn init -t next --yes` for a fresh app or `npx kitcn init --yes` for in-place adoption.
73
+
74
+ On local Convex, `add auth --yes` also finishes the first auth bootstrap pass: generated runtime, `BETTER_AUTH_SECRET`, and `JWKS`.
75
+
76
+ ### 2. Auth Config
77
+
78
+ ```ts
79
+ // convex/functions/auth.config.ts
80
+ import { getAuthConfigProvider } from 'kitcn/auth/config';
81
+ import { getEnv } from '../lib/get-env';
82
+
83
+ export default {
84
+ providers: [
85
+ getEnv().JWKS
86
+ ? getAuthConfigProvider({ jwks: getEnv().JWKS })
87
+ : getAuthConfigProvider(),
88
+ ],
89
+ } satisfies AuthConfig;
90
+ ```
91
+
92
+ ### 3. Generate Runtime
93
+
94
+ Start `kitcn dev` for the long-running local runtime. It runs Convex,
95
+ watches for changes, and regenerates runtime files automatically:
96
+
97
+ ```bash
98
+ npx kitcn dev
99
+ ```
100
+
101
+ ### 4. Define Auth Contract
102
+
103
+ The auth definition lives at `<functionsDir>/auth.ts`. `functionsDir` comes
104
+ from `convex.json.functions` (default: `convex`), so scaffolded kitcn
105
+ apps use `convex/functions/auth.ts`.
106
+
107
+ ```ts
108
+ // convex/functions/auth.ts
109
+ import { convex } from 'kitcn/auth';
110
+ import { getEnv } from '../lib/get-env';
111
+ import authConfig from './auth.config';
112
+ import { defineAuth } from './generated/auth';
113
+
114
+ export default defineAuth(() => ({
115
+ emailAndPassword: {
116
+ enabled: true,
117
+ },
118
+ baseURL: getEnv().SITE_URL,
119
+ plugins: [
120
+ convex({
121
+ authConfig,
122
+ jwks: getEnv().JWKS,
123
+ }),
124
+ ],
125
+ session: {
126
+ expiresIn: 60 * 60 * 24 * 30,
127
+ updateAge: 60 * 60 * 24 * 15,
128
+ },
129
+ telemetry: { enabled: false },
130
+ trustedOrigins: [getEnv().SITE_URL],
131
+ }));
132
+ ```
133
+
134
+ Use runtime exports (`getAuth`, CRUD/JWKS handlers, trigger handlers, static `auth`) from `<functionsDir>/generated/auth`.
135
+
136
+ ### 5. Schema (ORM API)
137
+
138
+ Default kitcn path:
139
+
140
+ ```bash
141
+ npx kitcn add auth --yes
142
+ npx kitcn add auth --schema --yes
143
+ ```
144
+
145
+ That path patches auth-owned table blocks directly into `<functionsDir>/schema.ts`
146
+ and records ownership in `<functionsDir>/plugins.lock.json`.
147
+
148
+ Raw Convex path:
149
+
150
+ ```bash
151
+ npx kitcn add auth --preset convex --yes
152
+ ```
153
+
154
+ That path refreshes `<functionsDir>/authSchema.ts` and patches
155
+ `<functionsDir>/schema.ts`. It assumes the raw Convex app is already
156
+ initialized and does not support `--schema`.
157
+
158
+ If you want to own the auth tables by hand, use `setup/server.md`.
159
+
160
+ ### 6. Auth HTTP Runtime
161
+
162
+ Import auth route helpers from `kitcn/auth/http`.
163
+ That entrypoint auto-installs the Convex-safe `MessageChannel` polyfill.
164
+
165
+ ### 7. HTTP Routes
166
+
167
+ Three options — cRPC (recommended), plain Convex, or Hono:
168
+
169
+ ```ts
170
+ // convex/functions/http.ts — cRPC option
171
+ import { authMiddleware } from 'kitcn/auth/http';
172
+ import { createHttpRouter } from 'kitcn/server';
173
+ import { Hono } from 'hono';
174
+ import { cors } from 'hono/cors';
175
+ import { getEnv } from '../lib/get-env';
176
+ import { getAuth } from './generated/auth';
177
+
178
+ const app = new Hono();
179
+ app.use('/api/*', cors({
180
+ origin: getEnv().SITE_URL,
181
+ allowHeaders: ['Content-Type', 'Authorization', 'Better-Auth-Cookie'],
182
+ exposeHeaders: ['Set-Better-Auth-Cookie'],
183
+ credentials: true,
184
+ }));
185
+ app.use(authMiddleware(getAuth));
186
+ export default createHttpRouter(app, httpRouter);
187
+ ```
188
+
189
+ ### 8. Environment Variables
190
+
191
+ ```bash
192
+ # convex/.env
193
+ SITE_URL=http://localhost:3000
194
+ GOOGLE_CLIENT_ID=...
195
+ GOOGLE_CLIENT_SECRET=...
196
+ # Auto-generated during local auth bootstrap and prod env sync
197
+ BETTER_AUTH_SECRET=...
198
+ JWKS=...
199
+ ```
200
+
201
+ Local Convex:
202
+ 1. `init --yes`, `dev`, and `add auth --yes` drive the first auth bootstrap when they own the flow.
203
+ 2. While `kitcn dev` is running on backend `convex`, later edits to `convex/.env` auto-sync.
204
+ 3. For the normal local path, `SITE_URL` should stay on `http://localhost:3000`.
205
+
206
+ Remote / repair:
207
+ 1. Use `npx kitcn env push` when the target deployment is already active.
208
+ 2. Use `npx kitcn env push --prod` for production sync.
209
+
210
+ Key rotation: `npx kitcn env push --rotate` (invalidates all tokens).
211
+
212
+ ---
213
+
214
+ ## Server Helpers
215
+
216
+ ```ts
217
+ import { getAuthUserIdentity, getAuthUserId, getSession, getHeaders } from 'kitcn/auth';
218
+ ```
219
+
220
+ | Helper | Returns | Use case |
221
+ |--------|---------|----------|
222
+ | `getAuthUserIdentity(ctx)` | `{ userId, sessionId, subject }` or null | Full identity |
223
+ | `getAuthUserId(ctx)` | `Id<'user'>` or null | Just user ID |
224
+ | `getSession(ctx)` | `{ id, userId, activeOrganizationId, expiresAt }` or null | Session doc |
225
+ | `getHeaders(ctx)` | `Headers` with Authorization + x-forwarded-for | Forward to external APIs |
226
+
227
+ ```ts
228
+ // Common pattern
229
+ const userId = await getAuthUserId(ctx);
230
+ if (!userId) throw new CRPCError({ code: 'UNAUTHORIZED' });
231
+ const user = await ctx.orm.query.user.findFirst({ where: { id: userId } });
232
+ ```
233
+
234
+ ### Convex Plugin Options
235
+
236
+ ```ts
237
+ convex({
238
+ authConfig, // required
239
+ jwks: process.env.JWKS, // static JWKS for fast validation
240
+ jwt: {
241
+ expirationSeconds: 60 * 60 * 4, // default 15 min
242
+ definePayload: ({ user, session }) => ({
243
+ name: user.name, email: user.email, role: user.role,
244
+ sessionId: session.id, // always added automatically
245
+ }),
246
+ },
247
+ options: { basePath: '/custom/auth/path' }, // if non-default
248
+ })
249
+ ```
250
+
251
+ Default `definePayload` includes all user fields except `id` and `image`, plus `sessionId` and `iat`.
252
+
253
+ ---
254
+
255
+ ## Client Setup
256
+
257
+ ### Auth Client
258
+
259
+ ```ts
260
+ // src/lib/convex/auth-client.ts
261
+ import { inferAdditionalFields } from 'better-auth/client/plugins';
262
+ import { createAuthClient } from 'better-auth/react';
263
+ import { convexClient } from 'kitcn/auth/client';
264
+ import { createAuthMutations } from 'kitcn/react';
265
+ import type { Auth } from '@convex/auth-shared';
266
+
267
+ export const authClient = createAuthClient({
268
+ baseURL: process.env.NEXT_PUBLIC_SITE_URL!,
269
+ plugins: [inferAdditionalFields<Auth>(), convexClient()],
270
+ });
271
+
272
+ export const {
273
+ useSignInMutationOptions,
274
+ useSignInSocialMutationOptions,
275
+ useSignOutMutationOptions,
276
+ useSignUpMutationOptions,
277
+ } = createAuthMutations(authClient);
278
+ ```
279
+
280
+ ### Sign In
281
+
282
+ **Social:**
283
+ ```ts
284
+ const signInSocial = useMutation(useSignInSocialMutationOptions());
285
+ signInSocial.mutate({ callbackURL: window.location.origin, provider: 'google' });
286
+ ```
287
+
288
+ **Email/password** (requires `emailAndPassword: { enabled: true }` in server config):
289
+ ```ts
290
+ const signIn = useMutation(useSignInMutationOptions({ onSuccess: () => router.push('/') }));
291
+ signIn.mutate({ callbackURL: window.location.origin, email, password });
292
+
293
+ const signUp = useMutation(useSignUpMutationOptions({ onSuccess: () => router.push('/') }));
294
+ signUp.mutate({ callbackURL: window.location.origin, email, name, password });
295
+ ```
296
+
297
+ ### Sign Out
298
+
299
+ ```ts
300
+ const signOut = useMutation(useSignOutMutationOptions({
301
+ onSuccess: () => router.push('/login'),
302
+ }));
303
+ signOut.mutate();
304
+ ```
305
+
306
+ `useSignOutMutationOptions` auto-calls `unsubscribeAuthQueries()` before signOut to prevent UNAUTHORIZED errors. `isPending` stays true until token actually cleared.
307
+
308
+ ---
309
+
310
+ ## Client Hooks
311
+
312
+ All from `kitcn/react`:
313
+
314
+ | Hook | Returns | Description |
315
+ |------|---------|-------------|
316
+ | `useAuth()` | `{ hasSession, isAuthenticated, isLoading }` | Full auth state |
317
+ | `useMaybeAuth()` | `boolean` | Has token (optimistic, may not be verified) |
318
+ | `useIsAuth()` | `boolean` | Server-verified authentication |
319
+ | `useAuthGuard()` | `() => boolean` | Guard mutations, returns true if blocked |
320
+
321
+ ### useAuthGuard
322
+
323
+ ```ts
324
+ const guard = useAuthGuard();
325
+ const handleClick = () => {
326
+ if (guard()) return; // blocked — not authenticated
327
+ createPost.mutate({ title: 'New Post' });
328
+ };
329
+
330
+ // Or with callback — only runs if authenticated:
331
+ guard(async () => {
332
+ await createPost.mutateAsync({ title: 'New Post' });
333
+ });
334
+ ```
335
+
336
+ ## Conditional Rendering
337
+
338
+ All from `kitcn/react`:
339
+
340
+ | Component | Renders when |
341
+ |-----------|-------------|
342
+ | `MaybeAuthenticated` | Has session token (optimistic) |
343
+ | `Authenticated` | Server-verified authenticated |
344
+ | `MaybeUnauthenticated` | No session token (optimistic) |
345
+ | `Unauthenticated` | Server-verified not authenticated |
346
+
347
+ ```tsx
348
+ <MaybeAuthenticated><Dashboard /></MaybeAuthenticated>
349
+ <MaybeUnauthenticated><LoginPage /></MaybeUnauthenticated>
350
+ ```
351
+
352
+ ## Provider Config
353
+
354
+ ```tsx
355
+ <ConvexAuthProvider
356
+ client={convex}
357
+ authClient={authClient}
358
+ initialToken={token} // from SSR (caller.getToken())
359
+ onMutationUnauthorized={() => router.push('/login')}
360
+ onQueryUnauthorized={({ queryName }) => console.log(`Unauth: ${queryName}`)}
361
+ >
362
+ ```
363
+
364
+ For `@convex-dev/auth` (React Native):
365
+ ```tsx
366
+ import { ConvexProviderWithAuth } from 'kitcn/react';
367
+ <ConvexProviderWithAuth client={convex} useAuth={useAuthFromConvexDev}>
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Auth Triggers
373
+
374
+ Define triggers in `auth.ts` via `defineAuth(() => ({ triggers }))`. Triggers run inline in the same CRUD transaction.
375
+
376
+ ### Trigger Shape
377
+
378
+ Nested `{ create, update, delete, change }` per table, matching ORM `defineTriggers` pattern. See [Trigger Shape reference](#trigger-shape-1) below for callback signatures.
379
+
380
+ `before` return contract: `void` (continue unchanged), `{ data }` (shallow merge into payload), `false` (cancel write).
381
+
382
+ `change` receives `{ operation: 'insert' | 'update' | 'delete', id, newDoc, oldDoc }`.
383
+
384
+ ```ts
385
+ triggers: {
386
+ user: {
387
+ create: {
388
+ before: async (data, triggerCtx) => {
389
+ const username = await generateUniqueUsername(triggerCtx, data.name);
390
+ const role = adminEmails.includes(data.email) ? 'admin' : 'user';
391
+ return { data: { ...data, username, role } };
392
+ },
393
+ after: async (user, triggerCtx) => {
394
+ await triggerCtx.orm.insert(profiles).values({ userId: user.id, bio: '' });
395
+ const emailCaller = createEmailsCaller(triggerCtx);
396
+ await emailCaller.schedule.now.sendWelcome({ userId: user.id });
397
+ },
398
+ },
399
+ update: {
400
+ after: async (newDoc, triggerCtx) => {
401
+ // Use `change` handler for old vs new comparisons
402
+ },
403
+ },
404
+ delete: {
405
+ after: async (user, triggerCtx) => {
406
+ const profiles = await triggerCtx.orm.query.profiles.findMany({ where: { userId: user.id }, limit: 1000 });
407
+ for (const p of profiles) await triggerCtx.orm.delete(profilesTable).where(eq(profilesTable.id, p.id));
408
+ },
409
+ },
410
+ change: async (change, triggerCtx) => {
411
+ switch (change.operation) {
412
+ case 'update':
413
+ if (change.newDoc.image !== change.oldDoc.image) {
414
+ const profile = await triggerCtx.orm.query.profiles.findFirst({ where: { userId: change.id } });
415
+ if (profile) await triggerCtx.orm.update(profiles).set({ avatar: change.newDoc.image }).where(eq(profiles.id, profile.id));
416
+ }
417
+ break;
418
+ }
419
+ },
420
+ },
421
+ }
422
+ ```
423
+
424
+ ### Session Triggers
425
+
426
+ ```ts
427
+ triggers: {
428
+ session: {
429
+ create: {
430
+ after: async (session, triggerCtx) => {
431
+ if (!session.activeOrganizationId) {
432
+ const user = await triggerCtx.orm.query.user.findFirst({ where: { id: session.userId } });
433
+ if (user?.lastActiveOrganizationId) {
434
+ await triggerCtx.orm.update(sessionTable).set({ activeOrganizationId: user.lastActiveOrganizationId })
435
+ .where(eq(sessionTable.id, session.id));
436
+ }
437
+ }
438
+ },
439
+ },
440
+ },
441
+ }
442
+ ```
443
+
444
+ ### Type Safety
445
+
446
+ Triggers are typed from schema: `data` is `Infer<Schema['tables']['user']['validator']>`, `doc` includes `id` and `_creationTime`, `update` is `Partial`.
447
+
448
+ ---
449
+
450
+ ## Auth vs DB Triggers
451
+
452
+ Auth triggers (`defineAuth(...).triggers`) handle auth lifecycle events. DB triggers (`defineTriggers`) handle database-level side effects (aggregates, cascades, counters).
453
+
454
+ ---
455
+
456
+ ## API Reference
457
+
458
+ ### Trigger Shape
459
+
460
+ Nested `{ create, update, delete, change }` per table, matching ORM `defineTriggers` pattern:
461
+
462
+ | Hook | Signature | Return |
463
+ |------|-----------|--------|
464
+ | `create.before` | `(data, ctx) => void \| { data } \| false` | Merge / cancel |
465
+ | `create.after` | `(doc, ctx) => void` | Side effects |
466
+ | `update.before` | `(update, ctx) => void \| { data } \| false` | Merge / cancel |
467
+ | `update.after` | `(newDoc, ctx) => void` | Sync changes |
468
+ | `delete.before` | `(doc, ctx) => void \| { data } \| false` | Guard / cancel |
469
+ | `delete.after` | `(doc, ctx) => void` | Cleanup |
470
+ | `change` | `(change, ctx) => void` | Cross-operation |
@@ -0,0 +1,153 @@
1
+ # Create Plugins
2
+
3
+ Canonical patterns for new kitcn plugins.
4
+
5
+ ## Goals
6
+
7
+ 1. Keep runtime bundles entry-local.
8
+ 2. Keep schema extension ownership explicit and local.
9
+ 3. Keep scaffolds predictable and user-owned.
10
+ 4. Keep CLI generic; plugin behavior comes from plugin manifests.
11
+
12
+ ## Package Layout
13
+
14
+ Use split ownership:
15
+
16
+ 1. `@kitcn/<plugin>`: runtime capability, middleware, stable helpers, shared types.
17
+ 2. `packages/kitcn/src/cli/plugins/<plugin>/...`: scaffold templates for local schema/runtime files.
18
+ 3. `convex/lib/plugins/<plugin>/schema.ts`: app-owned schema extension generated by the CLI.
19
+
20
+ Do not ship first-party schema entrypoints from plugin packages.
21
+
22
+ ## Runtime API Contract
23
+
24
+ Use middleware-scoped access with one runtime primitive:
25
+
26
+ ```ts
27
+ // app code
28
+ import { MyPlugin } from '@kitcn/my-plugin';
29
+ import { createPluginsMyPluginCaller } from './generated/plugins/my-plugin.runtime';
30
+ import { privateMutation } from './lib/crpc';
31
+
32
+ export const myPlugin = MyPlugin.configure({
33
+ enabled: true,
34
+ });
35
+
36
+ export const myProcedure = privateMutation.use(myPlugin.middleware())
37
+ .mutation(async ({ ctx }) => {
38
+ const caller = createPluginsMyPluginCaller(ctx);
39
+ await caller.doWork({});
40
+ });
41
+ ```
42
+
43
+ Plugin packages should define their runtime entry with `definePlugin('<key>', ...)` from `kitcn/plugins` and expose a named plugin value such as `ResendPlugin`.
44
+
45
+ Rules:
46
+
47
+ 1. No `ctx.getApi(...)` runtime path.
48
+ 2. Middleware injects plugin runtime surface at `ctx.api.<plugin>`.
49
+ 3. Use generated callers (`createPlugins<Plugin>Caller(ctx)`) for internal function composition.
50
+ 4. Reusable defaults go on plugin chain via `.configure(...)`.
51
+ 5. App env resolution belongs in scaffold/app code, not inside the package plugin. Package plugins should not read `process.env` for app secrets.
52
+ 6. Extend plugins with `.extend(({ middleware }) => ({ middleware: () => middleware().pipe(...), ...namedPresets }))`.
53
+ 7. `plugin.middleware()` still injects `ctx.api.<plugin>`; middleware overrides should build on the helper `middleware()`, not replace injection from scratch.
54
+ 8. Prefer object config.
55
+ 9. Use callback config only when context is required.
56
+ 10. No helper alternatives like `getMyPlugin(...)`.
57
+ 11. Plugin runtime code must use ORM context (`ctx.orm`) when it touches kitcn schema.
58
+
59
+ ## Private Procedure Contract
60
+
61
+ Plugin internal Convex functions should be scaffold-owned cRPC private procedures.
62
+
63
+ Rules:
64
+
65
+ 1. Reuse project cRPC builders from `convex/<paths.lib>/crpc.ts`.
66
+ 2. Define plugin internals with `privateQuery`, `privateMutation`, `privateAction` and chain plugin middleware per procedure.
67
+ 3. Do not define plugin internals with vanilla `internalQuery/internalMutation/internalAction`.
68
+ 4. Do not ship `create*Runtime` factory patterns from plugin packages.
69
+ 5. Do not ship `build*Handlers` wrapper factories from plugin packages.
70
+ 6. Do not use `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction` for plugin internal composition. Use `create<Module>Caller(ctx)` from `generated/<module>.runtime`.
71
+ 7. Hook/callback fanout should be scaffold-owned internal procedures, not dynamic function-handle config.
72
+
73
+ ## Schema Extension Contract
74
+
75
+ Schema lives in local scaffolded files and is defined with `defineSchemaExtension(...)`.
76
+
77
+ Expected shape:
78
+
79
+ 1. `defineSchemaExtension('<key>', { ...tables })`
80
+ 2. optional chained `.relations((r) => ({ ... }))`
81
+ 3. optional chained `.triggers({ ... })`
82
+
83
+ Canonical install:
84
+
85
+ ```ts
86
+ import { defineSchema } from 'kitcn/orm';
87
+ import { myExtension } from '../lib/plugins/my-plugin/schema';
88
+
89
+ export default defineSchema(tables).extend(myExtension());
90
+ ```
91
+
92
+ Relation composition rules:
93
+
94
+ 1. Extension `relations(...)` is merged before app `defineSchema(...).relations(...)`.
95
+ 2. Duplicate relation fields (`table.field`) throw.
96
+ 3. Use extension relation defaults for baseline wiring; app-level relations should extend, not duplicate fields.
97
+
98
+ Trigger composition rules:
99
+
100
+ 1. Extension triggers are merged before app triggers.
101
+ 2. Duplicate trigger hooks for the same table/event throw.
102
+
103
+ ## Scaffold Rules
104
+
105
+ 1. Local schema file lives at `convex/<paths.lib>/plugins/<plugin>/schema.ts`.
106
+ 2. Plugin Convex function exports live in `<functionsDir>/plugins/<plugin>.ts`.
107
+ 3. Non-function helpers live under `convex/<paths.lib>/plugins/<plugin>/...`.
108
+ 4. `kitcn codegen` must not generate plugin runtime modules.
109
+ 5. Scaffold templates need stable template IDs.
110
+ 6. `add` can merge/upsert scaffold mappings; never clobber custom files unless overwrite is explicit.
111
+ 7. `add --dry-run`, `add --diff [path]`, and `add --view [path]` preview one shared install plan: scaffold files, env bootstrap, `kitcn.json`, schema registration, lockfile write, dependency install status, codegen/hooks, env reminders.
112
+ 8. Preview comparisons for `.ts`, `.tsx`, `.js`, `.jsx`, and `.json` should be semantic enough to ignore formatter-only churn.
113
+ 9. `view` is read-only plan inspection. Default template source is lockfile mappings, fallback is the resolved preset, `--preset` forces preset selection.
114
+ 10. `info` audits installed plugins from schema + lockfile and reports scaffold drift / missing dependencies.
115
+ 11. Runtime packages should stay focused on stable logic; function wiring and schema stay local.
116
+ 12. If `paths.env` is missing, `kitcn add <plugin>` bootstraps `${paths.lib}/get-env.ts` and writes `paths.env` into `kitcn.json`.
117
+ 13. `envFields` can attach reminder metadata; `kitcn add <plugin>` prints those reminders against `<functionsDir>/.env` and includes them in JSON output.
118
+ 14. Scaffolded Convex files should read env through `getEnv()` only. Do not generate `process.env` access in plugin scaffolds.
119
+
120
+ ## Lockfile Rules
121
+
122
+ Lockfile path is fixed:
123
+
124
+ `<functionsDir>/plugins.lock.json`
125
+
126
+ Current contract:
127
+
128
+ 1. `plugins.<plugin>.package = <packageName>` (required)
129
+ 2. optional `plugins.<plugin>.files.<templateId> = <relativePath>`
130
+
131
+ No `version` or timestamp fields in lockfile.
132
+
133
+ ## CLI Manifest Guidance
134
+
135
+ Each plugin should provide:
136
+
137
+ 1. `label` / `description` for prompts plus `view` / `info` output
138
+ 2. `docs` links and `keywords` for `kitcn docs`
139
+ 3. preset definitions
140
+ 4. scaffold template resolver
141
+ 5. local schema registration metadata (`importName`, file path, target root)
142
+
143
+ CLI stays plugin-agnostic; plugin-specific flags should not be added globally.
144
+
145
+ ## Test Checklist
146
+
147
+ 1. plugin ctx typing: `ctx.api.<plugin>` available only after `.use(plugin.middleware())`
148
+ 2. codegen: no plugin runtime artifacts are generated
149
+ 3. stale cleanup: `generated/plugins/**` artifacts are removed
150
+ 4. add flow: idempotent scaffold writes + lockfile mapping
151
+ 5. preview flow: plan captures changed/missing scaffold files plus schema/lockfile/config/env updates
152
+ 6. runtime parity tests for plugin API methods exposed via middleware
153
+ 7. schema registration: local `convex/lib/plugins/<plugin>/schema.ts` is scaffolded and imported once in root schema