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.
- package/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13255 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -35
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +759 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- 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
|