devlyn-cli 0.3.0 → 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 CHANGED
@@ -105,10 +105,12 @@ During installation, you can choose to add optional skills and third-party skill
105
105
  | `cloudflare-nextjs-setup` | skill | Cloudflare Workers + Next.js deployment with OpenNext |
106
106
  | `generate-skill` | skill | Create well-structured Claude Code skills following Anthropic best practices |
107
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 |
108
109
  | `pyx-scan` | skill | Check whether an AI agent skill is safe before installing |
109
110
  | `vercel-labs/agent-skills` | pack | React, Next.js, React Native best practices |
110
111
  | `supabase/agent-skills` | pack | Supabase integration patterns |
111
112
  | `coreyhaines31/marketingskills` | pack | Marketing automation and content skills |
113
+ | `anthropics/skills` | pack | Official Anthropic skill-creator with eval framework and description optimizer |
112
114
 
113
115
  ## How It Works
114
116
 
package/bin/devlyn.js CHANGED
@@ -67,11 +67,13 @@ const OPTIONAL_ADDONS = [
67
67
  { name: 'cloudflare-nextjs-setup', desc: 'Cloudflare Workers + Next.js deployment with OpenNext', type: 'local' },
68
68
  { name: 'generate-skill', desc: 'Create well-structured Claude Code skills following Anthropic best practices', type: 'local' },
69
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' },
70
71
  { name: 'pyx-scan', desc: 'Check whether an AI agent skill is safe before installing', type: 'local' },
71
72
  // External skill packs (installed via npx skills add)
72
73
  { name: 'vercel-labs/agent-skills', desc: 'React, Next.js, React Native best practices', type: 'external' },
73
74
  { name: 'supabase/agent-skills', desc: 'Supabase integration patterns', type: 'external' },
74
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' },
75
77
  ];
76
78
 
77
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>