devlyn-cli 0.2.1 โ 0.4.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/README.md +3 -0
- package/bin/devlyn.js +3 -0
- package/optional-skills/better-auth-setup/SKILL.md +450 -0
- package/optional-skills/better-auth-setup/references/api-keys.md +236 -0
- package/optional-skills/better-auth-setup/references/config-and-entry.md +239 -0
- package/optional-skills/better-auth-setup/references/middleware.md +409 -0
- package/optional-skills/better-auth-setup/references/schema.md +224 -0
- package/optional-skills/better-auth-setup/references/testing.md +241 -0
- package/optional-skills/generate-skill/CHECKLIST.md +60 -0
- package/optional-skills/generate-skill/PROMPT-PATTERNS.md +370 -0
- package/optional-skills/generate-skill/REFERENCE.md +195 -0
- package/optional-skills/generate-skill/SKILL.md +178 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,11 +103,14 @@ During installation, you can choose to add optional skills and third-party skill
|
|
|
103
103
|
| Addon | Type | Description |
|
|
104
104
|
|---|---|---|
|
|
105
105
|
| `cloudflare-nextjs-setup` | skill | Cloudflare Workers + Next.js deployment with OpenNext |
|
|
106
|
+
| `generate-skill` | skill | Create well-structured Claude Code skills following Anthropic best practices |
|
|
106
107
|
| `prompt-engineering` | skill | Claude 4 prompt optimization using Anthropic best practices |
|
|
108
|
+
| `better-auth-setup` | skill | Production-ready Better Auth + Hono + Drizzle + PostgreSQL auth setup |
|
|
107
109
|
| `pyx-scan` | skill | Check whether an AI agent skill is safe before installing |
|
|
108
110
|
| `vercel-labs/agent-skills` | pack | React, Next.js, React Native best practices |
|
|
109
111
|
| `supabase/agent-skills` | pack | Supabase integration patterns |
|
|
110
112
|
| `coreyhaines31/marketingskills` | pack | Marketing automation and content skills |
|
|
113
|
+
| `anthropics/skills` | pack | Official Anthropic skill-creator with eval framework and description optimizer |
|
|
111
114
|
|
|
112
115
|
## How It Works
|
|
113
116
|
|
package/bin/devlyn.js
CHANGED
|
@@ -65,12 +65,15 @@ ${g} v${PKG.version} ${COLORS.dim}ยท ${k}๐ฉ by Donut Studio${r}
|
|
|
65
65
|
const OPTIONAL_ADDONS = [
|
|
66
66
|
// Local optional skills (copied to .claude/skills/)
|
|
67
67
|
{ name: 'cloudflare-nextjs-setup', desc: 'Cloudflare Workers + Next.js deployment with OpenNext', type: 'local' },
|
|
68
|
+
{ name: 'generate-skill', desc: 'Create well-structured Claude Code skills following Anthropic best practices', type: 'local' },
|
|
68
69
|
{ name: 'prompt-engineering', desc: 'Claude 4 prompt optimization using Anthropic best practices', type: 'local' },
|
|
70
|
+
{ name: 'better-auth-setup', desc: 'Production-ready Better Auth + Hono + Drizzle + PostgreSQL auth setup', type: 'local' },
|
|
69
71
|
{ name: 'pyx-scan', desc: 'Check whether an AI agent skill is safe before installing', type: 'local' },
|
|
70
72
|
// External skill packs (installed via npx skills add)
|
|
71
73
|
{ name: 'vercel-labs/agent-skills', desc: 'React, Next.js, React Native best practices', type: 'external' },
|
|
72
74
|
{ name: 'supabase/agent-skills', desc: 'Supabase integration patterns', type: 'external' },
|
|
73
75
|
{ name: 'coreyhaines31/marketingskills', desc: 'Marketing automation and content skills', type: 'external' },
|
|
76
|
+
{ name: 'anthropics/skills', desc: 'Official Anthropic skill-creator with eval framework and description optimizer', type: 'external' },
|
|
74
77
|
];
|
|
75
78
|
|
|
76
79
|
function log(msg, color = 'reset') {
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: better-auth-setup
|
|
3
|
+
description: >
|
|
4
|
+
Production-ready Better Auth integration for Bun + Hono + Drizzle + PostgreSQL projects.
|
|
5
|
+
Sets up email/password auth, session cookies, API key auth, organization/multi-tenant support,
|
|
6
|
+
email verification, CORS, security headers, auth middleware, tenant context, and test infrastructure
|
|
7
|
+
in one pass with zero gotchas. Use this skill whenever setting up authentication in a new
|
|
8
|
+
Hono/Bun project, adding Better Auth to an existing project, or when the user mentions Better Auth,
|
|
9
|
+
auth setup, login, signup, session management, API keys, or multi-tenant auth. Also use when
|
|
10
|
+
debugging auth issues in a Hono + Better Auth stack.
|
|
11
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
|
|
12
|
+
argument-hint: "[new-project-path or 'debug']"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Better Auth Production Setup
|
|
16
|
+
|
|
17
|
+
Set up a complete, production-hardened authentication system using Better Auth with Bun + Hono + Drizzle ORM + PostgreSQL. This skill incorporates lessons from real production deployments where each "gotcha" caused hours of debugging.
|
|
18
|
+
|
|
19
|
+
The setup produces a dual-auth system: session cookies for browser users and API keys for programmatic access, with multi-tenant organization support and plan-based access control.
|
|
20
|
+
|
|
21
|
+
## Reference Files
|
|
22
|
+
|
|
23
|
+
Read these when each step directs you to them:
|
|
24
|
+
|
|
25
|
+
- `${CLAUDE_SKILL_DIR}/references/schema.md` โ Complete Drizzle schema (auth, org, API key tables)
|
|
26
|
+
- `${CLAUDE_SKILL_DIR}/references/middleware.md` โ Auth middleware, tenant context, types, error handler
|
|
27
|
+
- `${CLAUDE_SKILL_DIR}/references/api-keys.md` โ Key generation, CRUD routes, security patterns
|
|
28
|
+
- `${CLAUDE_SKILL_DIR}/references/config-and-entry.md` โ Env config, error types, entry point wiring
|
|
29
|
+
- `${CLAUDE_SKILL_DIR}/references/testing.md` โ Test preload, seed factory, integration patterns
|
|
30
|
+
|
|
31
|
+
## Handling Input
|
|
32
|
+
|
|
33
|
+
Parse `$ARGUMENTS` to determine the mode:
|
|
34
|
+
|
|
35
|
+
- **Empty or project path**: Run the full setup workflow (Steps 1-11)
|
|
36
|
+
- **`debug`**: Skip to the verification checklist (Step 11) to diagnose an existing setup
|
|
37
|
+
- **Specific step number** (e.g., `step 3`): Jump to that step for targeted work
|
|
38
|
+
|
|
39
|
+
If `$ARGUMENTS` is empty, ask the user for the project path or confirm the current directory.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Workflow
|
|
44
|
+
|
|
45
|
+
Follow this exact sequence. Each step builds on the previous one, and skipping ahead causes cascading issues.
|
|
46
|
+
|
|
47
|
+
### Step 1: Configure Environment
|
|
48
|
+
|
|
49
|
+
**Entry:** Project has `package.json` with Hono and Drizzle installed.
|
|
50
|
+
**Exit:** `src/config.ts` exists with Zod validation, `.env` has `BETTER_AUTH_SECRET`.
|
|
51
|
+
|
|
52
|
+
Install dependencies:
|
|
53
|
+
```bash
|
|
54
|
+
bun add better-auth @better-auth/cli drizzle-orm pino resend zod
|
|
55
|
+
bun add -d drizzle-kit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Generate the auth secret immediately (do not use a placeholder):
|
|
59
|
+
```bash
|
|
60
|
+
bunx @better-auth/cli@latest secret
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Read `${CLAUDE_SKILL_DIR}/references/config-and-entry.md` for the complete Zod config implementation. Key env vars:
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Purpose |
|
|
66
|
+
|----------|----------|---------|
|
|
67
|
+
| `BETTER_AUTH_SECRET` | Yes | Token signing (min 32 chars) |
|
|
68
|
+
| `BETTER_AUTH_URL` | Yes | Base URL for auth service |
|
|
69
|
+
| `DATABASE_URL` | Yes | PostgreSQL connection string |
|
|
70
|
+
| `TRUSTED_ORIGINS` | Production | Comma-separated frontend origins |
|
|
71
|
+
| `CORS_ORIGIN` | No | CORS allowed origins (default: `*`) |
|
|
72
|
+
| `RESEND_API_KEY` | No | Email delivery (console fallback if absent) |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Step 2: Define Database Schema
|
|
77
|
+
|
|
78
|
+
**Entry:** Config validated at startup.
|
|
79
|
+
**Exit:** Migration applied, tables created in PostgreSQL.
|
|
80
|
+
|
|
81
|
+
Read `${CLAUDE_SKILL_DIR}/references/schema.md` for the complete schema. Key decisions:
|
|
82
|
+
|
|
83
|
+
- Use **plural table names** (`users`, `sessions`) โ mapped to Better Auth's singular models in Step 3
|
|
84
|
+
- Use **UUIDs** for all primary keys
|
|
85
|
+
- Add **indexes** on foreign keys and frequently-queried columns
|
|
86
|
+
- `api_keys` stores only the **hash**, never the plaintext
|
|
87
|
+
- `organizations` needs a `plan` column for tiered access control
|
|
88
|
+
|
|
89
|
+
After defining the schema, generate and apply migrations:
|
|
90
|
+
```bash
|
|
91
|
+
bunx drizzle-kit generate
|
|
92
|
+
bunx drizzle-kit migrate
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### Step 3: Create Better Auth Instance
|
|
98
|
+
|
|
99
|
+
**Entry:** Schema migrated, config module ready.
|
|
100
|
+
**Exit:** `src/lib/auth.ts` with all plugins and hooks configured.
|
|
101
|
+
|
|
102
|
+
This is the most critical file. It addresses every known production gotcha:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// src/lib/auth.ts
|
|
106
|
+
import { betterAuth } from "better-auth";
|
|
107
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
108
|
+
import { organization } from "better-auth/plugins";
|
|
109
|
+
|
|
110
|
+
export const auth = betterAuth({
|
|
111
|
+
// Always set basePath explicitly โ missing this causes route mismatches
|
|
112
|
+
// between where Hono mounts auth routes and where Better Auth handles them.
|
|
113
|
+
basePath: "/auth",
|
|
114
|
+
baseURL: config.BETTER_AUTH_URL,
|
|
115
|
+
secret: config.BETTER_AUTH_SECRET,
|
|
116
|
+
|
|
117
|
+
// Required for cross-origin cookie auth. Without this, a separate frontend
|
|
118
|
+
// app cannot authenticate because Better Auth rejects the origin.
|
|
119
|
+
trustedOrigins: config.TRUSTED_ORIGINS
|
|
120
|
+
? config.TRUSTED_ORIGINS.split(",").map((o) => o.trim()).filter(Boolean)
|
|
121
|
+
: [],
|
|
122
|
+
|
|
123
|
+
// Map plural Drizzle table names to Better Auth's singular model names.
|
|
124
|
+
// Better Auth expects "user", "session", etc. If your Drizzle schema uses
|
|
125
|
+
// "users", "sessions", you need this mapping or queries fail silently.
|
|
126
|
+
database: drizzleAdapter(db, {
|
|
127
|
+
provider: "pg",
|
|
128
|
+
schema: {
|
|
129
|
+
...schema,
|
|
130
|
+
user: schema.users,
|
|
131
|
+
session: schema.sessions,
|
|
132
|
+
account: schema.accounts,
|
|
133
|
+
verification: schema.verifications,
|
|
134
|
+
organization: schema.organizations,
|
|
135
|
+
member: schema.orgMemberships,
|
|
136
|
+
invitation: schema.invitations,
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
emailAndPassword: {
|
|
141
|
+
enabled: true,
|
|
142
|
+
minPasswordLength: 8,
|
|
143
|
+
requireEmailVerification: true,
|
|
144
|
+
sendResetPassword: async ({ user, url }) => {
|
|
145
|
+
await sendEmail({ to: user.email, subject: "Reset your password",
|
|
146
|
+
html: `<p>Click <a href="${url}">here</a> to reset your password.</p>` });
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
emailVerification: {
|
|
151
|
+
sendOnSignUp: true,
|
|
152
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
153
|
+
await sendEmail({ to: user.email, subject: "Verify your email",
|
|
154
|
+
html: `<p>Click <a href="${url}">here</a> to verify your email.</p>` });
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
session: { cookieCache: { enabled: true, maxAge: 5 * 60 } },
|
|
159
|
+
|
|
160
|
+
plugins: [organization()],
|
|
161
|
+
|
|
162
|
+
// Better Auth's organization plugin does NOT auto-create an org on signup.
|
|
163
|
+
// If your API requires tenant context (org membership), users will get 403
|
|
164
|
+
// on every request after signup. This hook creates a personal org automatically.
|
|
165
|
+
databaseHooks: {
|
|
166
|
+
user: {
|
|
167
|
+
create: {
|
|
168
|
+
after: async (user) => {
|
|
169
|
+
try {
|
|
170
|
+
const name = user.name || user.email.split("@")[0];
|
|
171
|
+
const orgSlug = `${slugify(name)}-${user.id.slice(-8)}`;
|
|
172
|
+
const [org] = await db.insert(schema.organizations)
|
|
173
|
+
.values({ name: `${name}'s Org`, slug: orgSlug, plan: "free" })
|
|
174
|
+
.returning();
|
|
175
|
+
await db.insert(schema.orgMemberships)
|
|
176
|
+
.values({ organizationId: org.id, userId: user.id, role: "owner" });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// Log but don't crash signup โ a missing org is recoverable,
|
|
179
|
+
// a failed signup is not.
|
|
180
|
+
console.error("Failed to create personal org:", user.id, error);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Gotchas This Config Addresses
|
|
190
|
+
|
|
191
|
+
| Setting | What happens if missing |
|
|
192
|
+
|---------|----------------------|
|
|
193
|
+
| `basePath: "/auth"` | Auth routes 404 because Better Auth handles at `/` while Hono mounts at `/auth` |
|
|
194
|
+
| `trustedOrigins` | Cross-origin cookie auth fails silently |
|
|
195
|
+
| Table name mapping | Drizzle queries fail silently (plural vs singular mismatch) |
|
|
196
|
+
| `databaseHooks` auto-org | New users 403 on all protected endpoints |
|
|
197
|
+
| `try/catch` in hook | Slug collision crashes signup instead of just skipping org creation |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Step 4: Set Up Email Delivery
|
|
202
|
+
|
|
203
|
+
**Entry:** Auth instance configured with email callbacks.
|
|
204
|
+
**Exit:** `src/lib/email.ts` with Resend integration and dev fallback.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// src/lib/email.ts
|
|
208
|
+
import { Resend } from "resend";
|
|
209
|
+
import { config } from "../config";
|
|
210
|
+
|
|
211
|
+
let resend: Resend | null = null;
|
|
212
|
+
function getResendClient(): Resend | null {
|
|
213
|
+
if (!config.RESEND_API_KEY) return null;
|
|
214
|
+
if (!resend) resend = new Resend(config.RESEND_API_KEY);
|
|
215
|
+
return resend;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function sendEmail({ to, subject, html }: { to: string; subject: string; html: string }) {
|
|
219
|
+
const client = getResendClient();
|
|
220
|
+
if (!client) {
|
|
221
|
+
console.log(`[EMAIL] To: ${to} | Subject: ${subject}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const { error } = await client.emails.send({ from: config.EMAIL_FROM, to, subject, html });
|
|
225
|
+
if (error) throw new Error(`Email delivery failed: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Test email leaks are a real risk โ when `RESEND_API_KEY` is in your `.env`, test signups send real emails. Step 10 addresses this by clearing the key in the test preload.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Step 5: Mount Auth Routes
|
|
234
|
+
|
|
235
|
+
**Entry:** Auth instance and email module ready.
|
|
236
|
+
**Exit:** `/auth/*` endpoints responding to signup, login, session, signout.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// src/routes/auth.ts
|
|
240
|
+
import { Hono } from "hono";
|
|
241
|
+
import { auth } from "../lib/auth";
|
|
242
|
+
|
|
243
|
+
export const authRoutes = new Hono();
|
|
244
|
+
|
|
245
|
+
// Pass c.req.raw (the standard Request object), NOT c or c.req.
|
|
246
|
+
// Better Auth expects a standard Request. Passing the Hono wrapper causes
|
|
247
|
+
// type errors or silent failures.
|
|
248
|
+
authRoutes.on(["POST", "GET"], "/*", (c) => auth.handler(c.req.raw));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### Step 6: Build Auth Middleware
|
|
254
|
+
|
|
255
|
+
**Entry:** Auth routes mounted.
|
|
256
|
+
**Exit:** `src/middleware/auth.ts` validates API keys and session cookies.
|
|
257
|
+
|
|
258
|
+
Read `${CLAUDE_SKILL_DIR}/references/middleware.md` for the complete implementation. The middleware:
|
|
259
|
+
- Detects API keys via `Authorization: Bearer pyx_...` prefix
|
|
260
|
+
- Validates via SHA-256 hash lookup + revocation + expiration checks
|
|
261
|
+
- Falls back to Better Auth session cookie validation
|
|
262
|
+
- Explicitly rejects non-`pyx_` Bearer tokens (prevents confusing silent failures)
|
|
263
|
+
- Updates `lastUsedAt` fire-and-forget (no request blocking)
|
|
264
|
+
- Returns generic 401 for all key failures (prevents key enumeration)
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### Step 7: Build Tenant Context Middleware
|
|
269
|
+
|
|
270
|
+
**Entry:** Auth middleware populates `AuthContext` on the request.
|
|
271
|
+
**Exit:** `src/middleware/tenant-context.ts` resolves org and plan.
|
|
272
|
+
|
|
273
|
+
Read `${CLAUDE_SKILL_DIR}/references/middleware.md` for the implementation. Key behaviors:
|
|
274
|
+
- API key auth: org already resolved from the key's project, just fetches plan
|
|
275
|
+
- Session auth: looks up user's most recent org membership
|
|
276
|
+
- Uses distinct error code `no_organization` (not generic `forbidden`) so the frontend can show "Create your first organization" instead of "Access denied"
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Step 8: Define Error Types
|
|
281
|
+
|
|
282
|
+
**Entry:** Middleware needs error classes to throw.
|
|
283
|
+
**Exit:** `src/lib/errors.ts` with `AppError` hierarchy, `src/middleware/error-handler.ts`.
|
|
284
|
+
|
|
285
|
+
Read `${CLAUDE_SKILL_DIR}/references/config-and-entry.md` for the complete error hierarchy and handler. Every error has a distinct `code` field for frontend differentiation:
|
|
286
|
+
|
|
287
|
+
| Error Class | Code | Status |
|
|
288
|
+
|-------------|------|--------|
|
|
289
|
+
| `AuthError` | `auth_required` | 401 |
|
|
290
|
+
| `ForbiddenError` | `forbidden` | 403 |
|
|
291
|
+
| `NotFoundError` | `not_found` | 404 |
|
|
292
|
+
| `ValidationError` | `validation_error` | 422 |
|
|
293
|
+
| `RateLimitError` | `rate_limited` | 429 |
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### Step 9: Wire Entry Point
|
|
298
|
+
|
|
299
|
+
**Entry:** All middleware and routes implemented.
|
|
300
|
+
**Exit:** `src/index.ts` with correct middleware ordering.
|
|
301
|
+
|
|
302
|
+
Read `${CLAUDE_SKILL_DIR}/references/config-and-entry.md` for the complete entry point. The middleware order is:
|
|
303
|
+
|
|
304
|
+
1. **Request ID** โ first, so error handler can include it in responses
|
|
305
|
+
2. **Security Headers** โ HSTS (production), X-Frame-Options
|
|
306
|
+
3. **CORS** โ before auth routes, otherwise OPTIONS preflight fails
|
|
307
|
+
4. **Logging** โ tracks all requests including auth failures
|
|
308
|
+
5. **Error Handler** โ catches `AppError` and returns consistent envelope
|
|
309
|
+
6. **Auth** (protected `/v1/*` only) โ validates credentials
|
|
310
|
+
7. **Tenant Context** (protected only) โ resolves org from auth
|
|
311
|
+
8. **Rate Limit** (protected only) โ plan-aware throttling
|
|
312
|
+
|
|
313
|
+
CORS before auth routes is non-negotiable. If registered after, preflight OPTIONS requests fail and browsers block all cross-origin requests.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Step 10: Set Up Test Infrastructure
|
|
318
|
+
|
|
319
|
+
**Entry:** Application fully functional.
|
|
320
|
+
**Exit:** Test preload, seed factory, integration app, cleanup utilities.
|
|
321
|
+
|
|
322
|
+
Read `${CLAUDE_SKILL_DIR}/references/testing.md` for the complete test setup. Essential components:
|
|
323
|
+
|
|
324
|
+
1. **Test preload** (`bunfig.toml` + `setup.ts`) โ clears `RESEND_API_KEY`, bridges `TEST_DATABASE_URL`
|
|
325
|
+
2. **Seed factory** (`seedTestData()`) โ creates full tenant hierarchy with unique slugs
|
|
326
|
+
3. **Integration app** (`createIntegrationApp()`) โ full middleware chain matching production
|
|
327
|
+
4. **Database cleanup** (`cleanupDatabase()`) โ deletes in FK dependency order
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Step 11: Verify Setup
|
|
332
|
+
|
|
333
|
+
**Entry:** All code implemented.
|
|
334
|
+
**Exit:** Every checklist item passes.
|
|
335
|
+
|
|
336
|
+
Run through each item. Every one represents a real production bug that was discovered the hard way.
|
|
337
|
+
|
|
338
|
+
<rules>
|
|
339
|
+
Do not skip any checklist item. Each one catches a specific class of bug.
|
|
340
|
+
If any item fails, fix the root cause before proceeding.
|
|
341
|
+
</rules>
|
|
342
|
+
|
|
343
|
+
**Auth Flow**
|
|
344
|
+
- [ ] Signup creates user + auto-creates personal org with `plan: "free"`
|
|
345
|
+
- [ ] Signup sends verification email (check console in dev)
|
|
346
|
+
- [ ] Login with verified email returns session cookie
|
|
347
|
+
- [ ] `GET /auth/session` returns session data
|
|
348
|
+
- [ ] `GET /auth/session` with invalid cookie returns `null` body (not `{ session: null }`)
|
|
349
|
+
- [ ] Signout destroys session
|
|
350
|
+
|
|
351
|
+
**API Key Auth**
|
|
352
|
+
- [ ] Creating API key returns plaintext once, stores only hash
|
|
353
|
+
- [ ] `Authorization: Bearer pyx_...` resolves to correct project and org
|
|
354
|
+
- [ ] Revoked keys return 401
|
|
355
|
+
- [ ] Expired keys return 401
|
|
356
|
+
- [ ] Non-`pyx_` Bearer tokens explicitly rejected
|
|
357
|
+
|
|
358
|
+
**Cross-Origin**
|
|
359
|
+
- [ ] OPTIONS preflight returns correct CORS headers
|
|
360
|
+
- [ ] Frontend on different origin can complete signup + login flow
|
|
361
|
+
- [ ] `credentials: true` set when using specific origins (not `*`)
|
|
362
|
+
- [ ] `TRUSTED_ORIGINS` includes frontend URL
|
|
363
|
+
|
|
364
|
+
**Tenant Context**
|
|
365
|
+
- [ ] API key auth resolves org from key's project
|
|
366
|
+
- [ ] Session auth resolves org from user's membership
|
|
367
|
+
- [ ] User with no org gets `no_organization` error code (not generic 403)
|
|
368
|
+
- [ ] No cross-tenant data access possible
|
|
369
|
+
|
|
370
|
+
**Security**
|
|
371
|
+
- [ ] `BETTER_AUTH_SECRET` is 32+ chars (generated, not placeholder)
|
|
372
|
+
- [ ] Dev secret rejected in production
|
|
373
|
+
- [ ] HSTS header present in production responses
|
|
374
|
+
- [ ] Error messages masked in production
|
|
375
|
+
- [ ] No key enumeration possible through auth error responses
|
|
376
|
+
|
|
377
|
+
**Testing**
|
|
378
|
+
- [ ] `RESEND_API_KEY` cleared in test setup (no real emails sent)
|
|
379
|
+
- [ ] Test database uses separate connection (`TEST_DATABASE_URL`)
|
|
380
|
+
- [ ] Seed data uses unique slugs (parallel test safety)
|
|
381
|
+
- [ ] Database cleanup respects FK dependency order
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## API Key System
|
|
386
|
+
|
|
387
|
+
For the complete custom API key implementation (generation, validation, CRUD routes), read `${CLAUDE_SKILL_DIR}/references/api-keys.md`.
|
|
388
|
+
|
|
389
|
+
Summary:
|
|
390
|
+
- **Format**: `pyx_` prefix + 32 random bytes base62-encoded (~50 chars)
|
|
391
|
+
- **Storage**: SHA-256 hash only โ plaintext never persisted
|
|
392
|
+
- **Validation**: Hash lookup โ revocation check โ expiration check โ resolve project/org
|
|
393
|
+
- **CRUD**: Create (returns plaintext once), List (shows prefix only), Revoke (soft delete)
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Slugify Utility
|
|
398
|
+
|
|
399
|
+
Shared across project creation and auto-org creation to prevent duplicate implementations:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// src/lib/slug.ts
|
|
403
|
+
export function slugify(text: string): string {
|
|
404
|
+
return text
|
|
405
|
+
.toLowerCase().trim()
|
|
406
|
+
.replace(/[^\w\s-]/g, "")
|
|
407
|
+
.replace(/[\s_]+/g, "-")
|
|
408
|
+
.replace(/^-+|-+$/g, "")
|
|
409
|
+
.slice(0, 50);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Always append a unique suffix (UUID slice or timestamp) to prevent collisions.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Quick Reference: Common Mistakes
|
|
418
|
+
|
|
419
|
+
| Mistake | Consequence | Prevention |
|
|
420
|
+
|---------|-------------|------------|
|
|
421
|
+
| Missing `basePath` | Auth routes 404 | Set `basePath: "/auth"` explicitly |
|
|
422
|
+
| Missing `trustedOrigins` | Cross-origin auth silent failure | Configure from `TRUSTED_ORIGINS` env |
|
|
423
|
+
| No table name mapping | Queries fail silently | Map plural to singular in adapter |
|
|
424
|
+
| No auto-org on signup | Users 403 everywhere | `databaseHooks.user.create.after` |
|
|
425
|
+
| CORS after auth routes | Preflight fails | CORS before all route handlers |
|
|
426
|
+
| `c.req` not `c.req.raw` | Type errors or silent failure | Always `auth.handler(c.req.raw)` |
|
|
427
|
+
| `RESEND_API_KEY` in tests | Real emails to test addresses | Clear in test preload |
|
|
428
|
+
| Generic 403 for no org | Frontend can't show helpful UX | Distinct `no_organization` code |
|
|
429
|
+
| `credentials: true` + `*` | Browser rejects response | Specific origins with credentials |
|
|
430
|
+
|
|
431
|
+
<example>
|
|
432
|
+
**User**: "Set up Better Auth in my new Hono project at ./my-api"
|
|
433
|
+
|
|
434
|
+
**Steps taken**:
|
|
435
|
+
1. Read project's package.json to confirm Hono + Drizzle
|
|
436
|
+
2. Install dependencies (better-auth, resend, zod, etc.)
|
|
437
|
+
3. Generate auth secret, create .env
|
|
438
|
+
4. Create src/config.ts with Zod validation
|
|
439
|
+
5. Create src/db/schema.ts with all auth + org + API key tables
|
|
440
|
+
6. Run drizzle-kit generate + migrate
|
|
441
|
+
7. Create src/lib/auth.ts with basePath, trustedOrigins, table mapping, databaseHooks
|
|
442
|
+
8. Create src/lib/email.ts with Resend + console fallback
|
|
443
|
+
9. Create src/routes/auth.ts with c.req.raw handler
|
|
444
|
+
10. Create src/middleware/auth.ts (dual-path: API key + session)
|
|
445
|
+
11. Create src/middleware/tenant-context.ts
|
|
446
|
+
12. Create src/lib/errors.ts + src/middleware/error-handler.ts
|
|
447
|
+
13. Create src/index.ts with correct middleware order
|
|
448
|
+
14. Create test utilities (setup.ts, db.ts, app.ts)
|
|
449
|
+
15. Run verification checklist
|
|
450
|
+
</example>
|