devlyn-cli 0.5.1 → 0.5.3
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/devlyn.js +1 -0
- package/optional-skills/better-auth-setup/SKILL.md +222 -11
- package/optional-skills/better-auth-setup/references/proxy-gotchas.md +148 -0
- package/optional-skills/better-auth-setup/references/proxy-setup.md +284 -0
- package/optional-skills/dokkit/ANALYSIS.md +198 -0
- package/optional-skills/dokkit/COMMANDS.md +365 -0
- package/optional-skills/dokkit/DOCX-XML.md +76 -0
- package/optional-skills/dokkit/EXPORT.md +102 -0
- package/optional-skills/dokkit/FILLING.md +377 -0
- package/optional-skills/dokkit/HWPX-XML.md +73 -0
- package/optional-skills/dokkit/IMAGE-SOURCING.md +127 -0
- package/optional-skills/dokkit/INGESTION.md +65 -0
- package/optional-skills/dokkit/SKILL.md +153 -0
- package/optional-skills/dokkit/STATE.md +60 -0
- package/optional-skills/dokkit/references/docx-field-patterns.md +151 -0
- package/optional-skills/dokkit/references/docx-structure.md +58 -0
- package/optional-skills/dokkit/references/field-detection-patterns.md +130 -0
- package/optional-skills/dokkit/references/hwpx-field-patterns.md +461 -0
- package/optional-skills/dokkit/references/hwpx-structure.md +159 -0
- package/optional-skills/dokkit/references/image-opportunity-heuristics.md +121 -0
- package/optional-skills/dokkit/references/image-xml-patterns.md +338 -0
- package/optional-skills/dokkit/references/section-image-interleaving.md +346 -0
- package/optional-skills/dokkit/references/section-range-detection.md +118 -0
- package/optional-skills/dokkit/references/state-schema.md +143 -0
- package/optional-skills/dokkit/references/supported-formats.md +67 -0
- package/optional-skills/dokkit/scripts/compile_hwpx.py +134 -0
- package/optional-skills/dokkit/scripts/detect_fields.py +301 -0
- package/optional-skills/dokkit/scripts/detect_fields_hwpx.py +286 -0
- package/optional-skills/dokkit/scripts/export_pdf.py +99 -0
- package/optional-skills/dokkit/scripts/parse_hwpx.py +185 -0
- package/optional-skills/dokkit/scripts/parse_image_with_gemini.py +159 -0
- package/optional-skills/dokkit/scripts/parse_xlsx.py +98 -0
- package/optional-skills/dokkit/scripts/source_images.py +365 -0
- package/optional-skills/dokkit/scripts/validate_docx.py +142 -0
- package/optional-skills/dokkit/scripts/validate_hwpx.py +281 -0
- package/optional-skills/dokkit/scripts/validate_state.py +132 -0
- package/package.json +1 -1
package/bin/devlyn.js
CHANGED
|
@@ -70,6 +70,7 @@ const OPTIONAL_ADDONS = [
|
|
|
70
70
|
{ name: 'prompt-engineering', desc: 'Claude 4 prompt optimization using Anthropic best practices', type: 'local' },
|
|
71
71
|
{ name: 'better-auth-setup', desc: 'Production-ready Better Auth + Hono + Drizzle + PostgreSQL auth setup', type: 'local' },
|
|
72
72
|
{ name: 'pyx-scan', desc: 'Check whether an AI agent skill is safe before installing', type: 'local' },
|
|
73
|
+
{ name: 'dokkit', desc: 'Document template filling for DOCX/HWPX — ingest, fill, review, export', type: 'local' },
|
|
73
74
|
// Local optional commands (copied to .claude/commands/)
|
|
74
75
|
{ name: 'devlyn.pencil-sync', desc: 'Sync designs between codebase and Pencil (.pen files) via MCP', type: 'command' },
|
|
75
76
|
// External skill packs (installed via npx skills add)
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: better-auth-setup
|
|
3
3
|
description: >
|
|
4
|
-
Production-ready Better Auth integration for
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Production-ready Better Auth integration for fullstack projects. Covers both the backend
|
|
5
|
+
(Bun + Hono + Drizzle + PostgreSQL) and the frontend reverse proxy architecture (Next.js,
|
|
6
|
+
Cloudflare Workers, or any framework proxying auth requests to a separate backend). Sets up
|
|
7
|
+
email/password auth, session cookies, OAuth providers (Google, GitHub), API key auth,
|
|
8
|
+
organization/multi-tenant support, email verification, CORS, security headers, auth middleware,
|
|
9
|
+
tenant context, proxy forwarding headers, dynamic baseURL with allowedHosts, cookie prefix
|
|
10
|
+
handling, and test infrastructure — all in one pass with zero gotchas. Use this skill whenever
|
|
11
|
+
setting up Better Auth, adding OAuth/social login, configuring a reverse proxy for auth,
|
|
12
|
+
debugging redirect_uri_mismatch errors, fixing state_mismatch cookie issues, session cookies
|
|
13
|
+
not persisting after OAuth callback, or when the user mentions Better Auth, OAuth proxy,
|
|
14
|
+
auth setup, login, signup, session management, API keys, multi-tenant auth, or
|
|
15
|
+
"session cookie not working".
|
|
11
16
|
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
|
|
12
|
-
argument-hint: "[new-project-path
|
|
17
|
+
argument-hint: "[new-project-path | 'debug' | 'proxy']"
|
|
13
18
|
---
|
|
14
19
|
|
|
15
20
|
# Better Auth Production Setup
|
|
16
21
|
|
|
17
|
-
Set up a complete, production-hardened authentication system using Better Auth
|
|
22
|
+
Set up a complete, production-hardened authentication system using Better Auth. This skill covers two deployment architectures:
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
1. **Backend-only** — Better Auth running directly in a Hono + Bun + Drizzle + PostgreSQL API server
|
|
25
|
+
2. **Fullstack with proxy** — A separate frontend (e.g., Next.js on Cloudflare Workers) that proxies auth requests to the backend
|
|
26
|
+
|
|
27
|
+
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. Every configuration choice addresses a real production gotcha that caused hours of debugging.
|
|
20
28
|
|
|
21
29
|
## Reference Files
|
|
22
30
|
|
|
@@ -27,17 +35,35 @@ Read these when each step directs you to them:
|
|
|
27
35
|
- `${CLAUDE_SKILL_DIR}/references/api-keys.md` — Key generation, CRUD routes, security patterns
|
|
28
36
|
- `${CLAUDE_SKILL_DIR}/references/config-and-entry.md` — Env config, error types, entry point wiring
|
|
29
37
|
- `${CLAUDE_SKILL_DIR}/references/testing.md` — Test preload, seed factory, integration patterns
|
|
38
|
+
- `${CLAUDE_SKILL_DIR}/references/proxy-setup.md` — Reverse proxy architecture, forwarding headers, OAuth callback routing
|
|
39
|
+
- `${CLAUDE_SKILL_DIR}/references/proxy-gotchas.md` — Proxy-specific troubleshooting (redirect_uri_mismatch, state_mismatch, cookie prefix issues)
|
|
30
40
|
|
|
31
41
|
## Handling Input
|
|
32
42
|
|
|
33
43
|
Parse `$ARGUMENTS` to determine the mode:
|
|
34
44
|
|
|
35
|
-
- **Empty or project path**:
|
|
45
|
+
- **Empty or project path**: Detect architecture, then run the full setup workflow
|
|
36
46
|
- **`debug`**: Skip to the verification checklist (Step 11) to diagnose an existing setup
|
|
47
|
+
- **`proxy`**: Skip to proxy-specific steps (Steps 12-14) for an existing backend
|
|
37
48
|
- **Specific step number** (e.g., `step 3`): Jump to that step for targeted work
|
|
38
49
|
|
|
39
50
|
If `$ARGUMENTS` is empty, ask the user for the project path or confirm the current directory.
|
|
40
51
|
|
|
52
|
+
## Step 0: Detect Architecture
|
|
53
|
+
|
|
54
|
+
Before starting, determine the deployment architecture:
|
|
55
|
+
|
|
56
|
+
1. Check if the project has a **separate frontend** that proxies to the backend (look for Next.js, proxy routes, `API_URL` env vars)
|
|
57
|
+
2. Check if the **current project IS the backend** (Hono, Express, or similar server framework with Better Auth)
|
|
58
|
+
|
|
59
|
+
| Architecture | Signals | Steps |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Backend-only | Hono/Express project, no frontend proxy | Steps 1-11 |
|
|
62
|
+
| Frontend proxy (setting up frontend) | Next.js/Remix project with `API_URL` pointing to a backend | Steps 12-14 |
|
|
63
|
+
| Fullstack (both) | Both projects accessible | Steps 1-14 |
|
|
64
|
+
|
|
65
|
+
If uncertain, ask the user which architecture they're using.
|
|
66
|
+
|
|
41
67
|
---
|
|
42
68
|
|
|
43
69
|
## Workflow
|
|
@@ -111,6 +137,9 @@ export const auth = betterAuth({
|
|
|
111
137
|
// Always set basePath explicitly — missing this causes route mismatches
|
|
112
138
|
// between where Hono mounts auth routes and where Better Auth handles them.
|
|
113
139
|
basePath: "/auth",
|
|
140
|
+
|
|
141
|
+
// For backend-only: use a static baseURL.
|
|
142
|
+
// For proxy architecture: use allowedHosts (see Step 12).
|
|
114
143
|
baseURL: config.BETTER_AUTH_URL,
|
|
115
144
|
secret: config.BETTER_AUTH_SECRET,
|
|
116
145
|
|
|
@@ -414,8 +443,163 @@ Always append a unique suffix (UUID slice or timestamp) to prevent collisions.
|
|
|
414
443
|
|
|
415
444
|
---
|
|
416
445
|
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Proxy Architecture (Steps 12-14)
|
|
449
|
+
|
|
450
|
+
These steps apply when the frontend is a separate application that proxies auth requests to the backend. Read `${CLAUDE_SKILL_DIR}/references/proxy-setup.md` for the complete guide with code examples.
|
|
451
|
+
|
|
452
|
+
### Step 12: Configure Dynamic baseURL on Backend
|
|
453
|
+
|
|
454
|
+
**Entry:** Backend auth works directly (Steps 1-11 complete or existing backend).
|
|
455
|
+
**Exit:** Backend derives baseURL per-request from forwarded headers.
|
|
456
|
+
|
|
457
|
+
Replace the static `baseURL` in `src/lib/auth.ts` with `allowedHosts`:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
export const auth = betterAuth({
|
|
461
|
+
baseURL: {
|
|
462
|
+
allowedHosts: [
|
|
463
|
+
"your-frontend.com", // Frontend domain (via proxy)
|
|
464
|
+
"your-backend.fly.dev", // Backend domain (direct access)
|
|
465
|
+
"localhost", // Local development
|
|
466
|
+
"*.fly.dev", // Platform internal routing
|
|
467
|
+
],
|
|
468
|
+
fallback: process.env.BETTER_AUTH_URL,
|
|
469
|
+
},
|
|
470
|
+
basePath: "/auth",
|
|
471
|
+
advanced: {
|
|
472
|
+
trustedProxyHeaders: true, // Read X-Forwarded-Host from the proxy
|
|
473
|
+
},
|
|
474
|
+
// ... rest of config unchanged
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Why `allowedHosts` instead of a static baseURL:**
|
|
479
|
+
- A static `baseURL` caches on first request (often a health check with internal hostname)
|
|
480
|
+
- `trustedProxyHeaders: true` alone does NOT work when `baseURL` is set — the static value takes precedence
|
|
481
|
+
- The `BETTER_AUTH_URL` env var also overrides forwarded headers
|
|
482
|
+
- `allowedHosts` derives baseURL per-request and never caches
|
|
483
|
+
|
|
484
|
+
Add Google OAuth (or other social providers):
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
socialProviders: {
|
|
488
|
+
google: {
|
|
489
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
490
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### Step 13: Configure Frontend Proxy
|
|
498
|
+
|
|
499
|
+
**Entry:** Backend uses `allowedHosts` with `trustedProxyHeaders`.
|
|
500
|
+
**Exit:** Frontend proxies auth requests and OAuth callbacks correctly.
|
|
501
|
+
|
|
502
|
+
The frontend proxy must do three things. Read `${CLAUDE_SKILL_DIR}/references/proxy-setup.md` for complete code.
|
|
503
|
+
|
|
504
|
+
#### 13a. Create two proxy routes
|
|
505
|
+
|
|
506
|
+
| Frontend Route | Backend Route | Purpose |
|
|
507
|
+
|---|---|---|
|
|
508
|
+
| `/api/auth/*` | `/auth/*` | Auth-client API calls (browser) |
|
|
509
|
+
| `/auth/*` | `/auth/*` | OAuth callbacks (Google redirects here) |
|
|
510
|
+
|
|
511
|
+
The second route is critical — Better Auth constructs OAuth callback URLs from `{baseURL}{basePath}/callback/{provider}`. When it derives baseURL from the frontend origin, the callback becomes `https://your-frontend.com/auth/callback/google`.
|
|
512
|
+
|
|
513
|
+
#### 13b. Set forwarding headers (not just strip them)
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// Strip user-provided headers to prevent spoofing
|
|
517
|
+
headers.delete("x-forwarded-for");
|
|
518
|
+
headers.delete("x-forwarded-host");
|
|
519
|
+
headers.delete("x-forwarded-proto");
|
|
520
|
+
headers.delete("x-real-ip");
|
|
521
|
+
headers.delete("cf-connecting-ip");
|
|
522
|
+
|
|
523
|
+
// Then set correct values from the actual request
|
|
524
|
+
headers.set("x-forwarded-host", url.host);
|
|
525
|
+
headers.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### 13c. Rewrite Location headers
|
|
529
|
+
|
|
530
|
+
When the backend returns a redirect, rewrite the `Location` header to point to the frontend origin.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
### Step 14: Configure Frontend Middleware
|
|
535
|
+
|
|
536
|
+
**Entry:** Proxy routes working.
|
|
537
|
+
**Exit:** Middleware detects session cookies correctly, including `__Secure-` prefix.
|
|
538
|
+
|
|
539
|
+
In production with HTTPS, Better Auth adds a `__Secure-` prefix to cookie names. Your middleware MUST check for both:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
const SESSION_COOKIES = [
|
|
543
|
+
"__Secure-better-auth.session_token", // Production (HTTPS)
|
|
544
|
+
"better-auth.session_token", // Development (HTTP)
|
|
545
|
+
];
|
|
546
|
+
|
|
547
|
+
const hasSession = SESSION_COOKIES.some(
|
|
548
|
+
(name) => !!request.cookies.get(name)?.value,
|
|
549
|
+
);
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
This is the most insidious gotcha in the proxy architecture — OAuth completes successfully, session is created, cookie is set, but the frontend middleware doesn't recognize it because it only checks the unprefixed name.
|
|
553
|
+
|
|
554
|
+
Configure the OAuth provider (Google Console example):
|
|
555
|
+
- **Authorized JavaScript origins**: `https://your-frontend.com`
|
|
556
|
+
- **Authorized redirect URIs**: `https://your-frontend.com/auth/callback/google`
|
|
557
|
+
|
|
558
|
+
The redirect URI uses `/auth/callback/google`, NOT `/api/auth/callback/google`.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
### Step 15: Verify Proxy Setup
|
|
563
|
+
|
|
564
|
+
In addition to the Step 11 checklist, verify:
|
|
565
|
+
|
|
566
|
+
**Proxy**
|
|
567
|
+
- [ ] Proxy route exists at `/api/auth/*` (for auth-client API calls)
|
|
568
|
+
- [ ] Proxy route exists at `/auth/*` (for OAuth callbacks)
|
|
569
|
+
- [ ] Proxy strips THEN sets `X-Forwarded-Host` and `X-Forwarded-Proto`
|
|
570
|
+
- [ ] Proxy rewrites `Location` headers from backend origin to frontend origin
|
|
571
|
+
|
|
572
|
+
**Dynamic baseURL**
|
|
573
|
+
- [ ] Backend uses `allowedHosts` (not a static `baseURL` string)
|
|
574
|
+
- [ ] `allowedHosts` includes frontend domain, backend domain, and `localhost`
|
|
575
|
+
- [ ] `fallback` is set for health checks
|
|
576
|
+
- [ ] `trustedProxyHeaders: true` is in `advanced` config
|
|
577
|
+
|
|
578
|
+
**OAuth**
|
|
579
|
+
- [ ] Google Console redirect URI is `https://{frontend}/auth/callback/google`
|
|
580
|
+
- [ ] Verification curl returns `redirect_uri` with frontend domain (not backend)
|
|
581
|
+
|
|
582
|
+
**Cookies**
|
|
583
|
+
- [ ] Middleware checks both `__Secure-better-auth.session_token` and `better-auth.session_token`
|
|
584
|
+
|
|
585
|
+
Test with:
|
|
586
|
+
```bash
|
|
587
|
+
curl -s -X POST "https://your-frontend.com/api/auth/sign-in/social" \
|
|
588
|
+
-H "Content-Type: application/json" \
|
|
589
|
+
-d '{"provider":"google","callbackURL":"/dashboard"}' | \
|
|
590
|
+
python3 -c "import sys,json,urllib.parse; data=json.load(sys.stdin); url=data.get('url',''); params=urllib.parse.parse_qs(urllib.parse.urlparse(url).query); print('redirect_uri:', params.get('redirect_uri',['N/A'])[0])"
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Expected: `redirect_uri: https://your-frontend.com/auth/callback/google`
|
|
594
|
+
|
|
595
|
+
For detailed troubleshooting of proxy-specific failures, read `${CLAUDE_SKILL_DIR}/references/proxy-gotchas.md`.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
417
599
|
## Quick Reference: Common Mistakes
|
|
418
600
|
|
|
601
|
+
### Backend Mistakes
|
|
602
|
+
|
|
419
603
|
| Mistake | Consequence | Prevention |
|
|
420
604
|
|---------|-------------|------------|
|
|
421
605
|
| Missing `basePath` | Auth routes 404 | Set `basePath: "/auth"` explicitly |
|
|
@@ -428,6 +612,18 @@ Always append a unique suffix (UUID slice or timestamp) to prevent collisions.
|
|
|
428
612
|
| Generic 403 for no org | Frontend can't show helpful UX | Distinct `no_organization` code |
|
|
429
613
|
| `credentials: true` + `*` | Browser rejects response | Specific origins with credentials |
|
|
430
614
|
|
|
615
|
+
### Proxy Mistakes
|
|
616
|
+
|
|
617
|
+
| Mistake | Consequence | Prevention |
|
|
618
|
+
|---------|-------------|------------|
|
|
619
|
+
| Static `baseURL` with proxy | OAuth callback uses wrong domain | Use `allowedHosts` + `fallback` |
|
|
620
|
+
| `trustedProxyHeaders` without `allowedHosts` | Static baseURL or env var overrides | `allowedHosts` bypasses priority chain |
|
|
621
|
+
| Only `/api/auth/*` proxy route | OAuth callback 404 (Google redirects to `/auth/callback/*`) | Add `/auth/*` proxy route |
|
|
622
|
+
| Proxy only strips headers | Backend doesn't know frontend origin | Strip THEN set `X-Forwarded-Host` |
|
|
623
|
+
| Middleware checks only `better-auth.session_token` | Authenticated users redirected to sign-in | Check both `__Secure-` prefixed and plain |
|
|
624
|
+
| Google Console redirect URI with `/api/auth/` | `redirect_uri_mismatch` from Google | Use `/auth/callback/google` (not `/api/auth/`) |
|
|
625
|
+
| `BETTER_AUTH_URL` env var set | Overrides forwarded headers | Use `allowedHosts` with `fallback` instead |
|
|
626
|
+
|
|
431
627
|
<example>
|
|
432
628
|
**User**: "Set up Better Auth in my new Hono project at ./my-api"
|
|
433
629
|
|
|
@@ -448,3 +644,18 @@ Always append a unique suffix (UUID slice or timestamp) to prevent collisions.
|
|
|
448
644
|
14. Create test utilities (setup.ts, db.ts, app.ts)
|
|
449
645
|
15. Run verification checklist
|
|
450
646
|
</example>
|
|
647
|
+
|
|
648
|
+
<example>
|
|
649
|
+
**User**: "I have a Next.js frontend and Hono backend. I need Google OAuth working through the proxy."
|
|
650
|
+
|
|
651
|
+
**Steps taken**:
|
|
652
|
+
1. Detect proxy architecture (Step 0)
|
|
653
|
+
2. Update backend auth.ts: replace static baseURL with allowedHosts + trustedProxyHeaders (Step 12)
|
|
654
|
+
3. Add Google social provider to auth config (Step 12)
|
|
655
|
+
4. Create /api/auth/* and /auth/* proxy routes on frontend (Step 13)
|
|
656
|
+
5. Set forwarding headers in proxy (Step 13)
|
|
657
|
+
6. Add Location header rewriting (Step 13)
|
|
658
|
+
7. Update frontend middleware to check both cookie names (Step 14)
|
|
659
|
+
8. Configure Google Console redirect URI (Step 14)
|
|
660
|
+
9. Run proxy verification checklist (Step 15)
|
|
661
|
+
</example>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Better Auth + Reverse Proxy — Gotchas & Troubleshooting
|
|
2
|
+
|
|
3
|
+
Common failures when running Better Auth behind a reverse proxy, with root causes and fixes. Each gotcha was discovered through real debugging — they are not obvious from the docs.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [redirect_uri_mismatch from Google](#1-redirect_uri_mismatch-from-google)
|
|
8
|
+
2. [state_mismatch after OAuth callback](#2-state_mismatch-after-oauth-callback)
|
|
9
|
+
3. [404 on all auth endpoints](#3-404-on-all-auth-endpoints)
|
|
10
|
+
4. [Session cookie not detected after login](#4-session-cookie-not-detected-after-login)
|
|
11
|
+
5. [trustedProxyHeaders not working](#5-trustedproxyheaders-not-working)
|
|
12
|
+
6. [baseURL caches wrong origin](#6-baseurl-caches-wrong-origin)
|
|
13
|
+
7. [BETTER_AUTH_URL env var overrides everything](#7-better_auth_url-env-var-overrides-everything)
|
|
14
|
+
8. [OAuth callback hits backend directly (skips proxy)](#8-oauth-callback-hits-backend-directly-skips-proxy)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. redirect_uri_mismatch from Google
|
|
19
|
+
|
|
20
|
+
**Symptom**: Google OAuth returns `redirect_uri_mismatch` error page.
|
|
21
|
+
|
|
22
|
+
**Root cause**: The `redirect_uri` Better Auth sends to Google doesn't match what's configured in Google Console. This happens when Better Auth derives the wrong baseURL — typically the backend's domain instead of the frontend's.
|
|
23
|
+
|
|
24
|
+
**Debug**: Run the verification curl to see what `redirect_uri` is actually being sent:
|
|
25
|
+
```bash
|
|
26
|
+
curl -s -X POST "https://your-frontend.com/api/auth/sign-in/social" \
|
|
27
|
+
-H "Content-Type: application/json" \
|
|
28
|
+
-d '{"provider":"google","callbackURL":"/dashboard"}' | \
|
|
29
|
+
python3 -c "import sys,json,urllib.parse; data=json.load(sys.stdin); url=data.get('url',''); params=urllib.parse.parse_qs(urllib.parse.urlparse(url).query); print('redirect_uri:', params.get('redirect_uri',['N/A'])[0])"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Compare the output to Google Console's "Authorized redirect URIs".
|
|
33
|
+
|
|
34
|
+
**Fix**:
|
|
35
|
+
- Ensure `allowedHosts` includes the frontend domain
|
|
36
|
+
- Ensure the proxy sets `X-Forwarded-Host` header (not just strips it)
|
|
37
|
+
- Ensure `trustedProxyHeaders: true` is in the `advanced` config
|
|
38
|
+
- Google Console redirect URI must be `https://{frontend}/auth/callback/{provider}` — note `/auth/`, NOT `/api/auth/`
|
|
39
|
+
|
|
40
|
+
**Google Console propagation**: Changes take up to 5 minutes. If you just updated, wait and retry.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. state_mismatch after OAuth callback
|
|
45
|
+
|
|
46
|
+
**Symptom**: After Google redirects back, Better Auth returns a `state_mismatch` error, typically visible at the backend URL.
|
|
47
|
+
|
|
48
|
+
**Root cause**: The state cookie was set on the frontend domain during OAuth initiation, but the callback arrived directly at the backend domain. Different domain = no cookie = state mismatch.
|
|
49
|
+
|
|
50
|
+
**Fix**: The OAuth callback URL must route through the frontend proxy, not directly to the backend. This means:
|
|
51
|
+
1. A proxy route at `/auth/*` must exist on the frontend (in addition to `/api/auth/*`)
|
|
52
|
+
2. The `redirect_uri` sent to the OAuth provider must use the frontend domain
|
|
53
|
+
3. Both the initial request and callback must share the same domain for cookies to work
|
|
54
|
+
|
|
55
|
+
**How to verify**: Check the `redirect_uri` in the OAuth initiation response. If it points to the backend domain, the `allowedHosts` or forwarding headers are misconfigured.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. 404 on all auth endpoints
|
|
60
|
+
|
|
61
|
+
**Symptom**: POST `/api/auth/sign-in/social` (or any auth endpoint) returns 404.
|
|
62
|
+
|
|
63
|
+
**Root causes** (multiple possible):
|
|
64
|
+
|
|
65
|
+
### 3a. baseURL includes a path component
|
|
66
|
+
Setting `baseURL` to something like `https://frontend.com/api` breaks Better Auth's internal routing. Better Auth uses `basePath` for path prefixing — `baseURL` should only be the origin (protocol + host).
|
|
67
|
+
|
|
68
|
+
### 3b. Host mismatch with static baseURL
|
|
69
|
+
If `baseURL` is `https://frontend.com` but requests arrive at the backend with `Host: backend.fly.dev`, Better Auth may reject the request because the host doesn't match. This is why dynamic `allowedHosts` is needed.
|
|
70
|
+
|
|
71
|
+
### 3c. allowedHosts array is empty or malformed
|
|
72
|
+
If the `allowedHosts` array doesn't match any incoming host AND no `fallback` is set, Better Auth has no baseURL to work with and may reject requests.
|
|
73
|
+
|
|
74
|
+
### 3d. Missing proxy route
|
|
75
|
+
If only `/api/auth/*` is proxied but the request arrives at `/auth/*` (OAuth callback), there's no route handler.
|
|
76
|
+
|
|
77
|
+
**Fix**: Use `allowedHosts` with all known domains, always set `fallback`, and ensure both proxy routes exist.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 4. Session cookie not detected after login
|
|
82
|
+
|
|
83
|
+
**Symptom**: OAuth completes successfully (no Google errors, callback returns 302 to `/dashboard`), but the user is immediately redirected to `/sign-in?redirect=%2Fdashboard`.
|
|
84
|
+
|
|
85
|
+
**Root cause**: In production with HTTPS, Better Auth prefixes cookie names with `__Secure-`. The cookie is actually named `__Secure-better-auth.session_token`, not `better-auth.session_token`. If middleware only checks the unprefixed name, it never finds the cookie.
|
|
86
|
+
|
|
87
|
+
**Why it happens with allowedHosts**: When `baseURL` is a dynamic config object (not a string), Better Auth can't determine the protocol at initialization time. In production (`NODE_ENV=production`), it defaults to secure cookies with the `__Secure-` prefix.
|
|
88
|
+
|
|
89
|
+
**Fix**: Middleware must check both cookie names:
|
|
90
|
+
```typescript
|
|
91
|
+
const SESSION_COOKIES = [
|
|
92
|
+
"__Secure-better-auth.session_token",
|
|
93
|
+
"better-auth.session_token",
|
|
94
|
+
];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**This is the most insidious gotcha** because everything appears to work — OAuth succeeds, session is created, cookie is set — but the frontend doesn't recognize it. Without the skill, Claude diagnoses this as "Set-Cookie header stripping" (plausible but wrong) instead of the `__Secure-` prefix (correct).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 5. trustedProxyHeaders not working
|
|
102
|
+
|
|
103
|
+
**Symptom**: Despite setting `trustedProxyHeaders: true`, Better Auth still uses the wrong origin for callback URLs.
|
|
104
|
+
|
|
105
|
+
**Root cause**: `trustedProxyHeaders` only takes effect when Better Auth actually reads forwarded headers. The `getBaseURL()` function has a priority order:
|
|
106
|
+
1. Static `baseURL` string (if set) — **always wins**
|
|
107
|
+
2. `BETTER_AUTH_URL` environment variable — **checked before headers**
|
|
108
|
+
3. `X-Forwarded-Host` / `X-Forwarded-Proto` headers — only reached if 1 and 2 are absent
|
|
109
|
+
4. Request URL — last resort
|
|
110
|
+
|
|
111
|
+
**Fix**: Use `allowedHosts` instead. It explicitly reads `X-Forwarded-Host` and constructs the baseURL per-request, bypassing the priority chain entirely.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 6. baseURL caches wrong origin
|
|
116
|
+
|
|
117
|
+
**Symptom**: Auth works for the first request after deploy, then breaks. Or auth never works because the first request was a health check.
|
|
118
|
+
|
|
119
|
+
**Root cause**: When using a static `baseURL` string or `trustedProxyHeaders` without `allowedHosts`, Better Auth resolves the baseURL once from the first request and caches it. If the first request is a health check (which arrives with `Host: backend.internal`), all subsequent requests use that cached value.
|
|
120
|
+
|
|
121
|
+
**Fix**: Use `allowedHosts` — it derives baseURL per-request and never caches.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 7. BETTER_AUTH_URL env var overrides everything
|
|
126
|
+
|
|
127
|
+
**Symptom**: You removed `baseURL` from the config to let `trustedProxyHeaders` work, but Better Auth still uses the wrong origin.
|
|
128
|
+
|
|
129
|
+
**Root cause**: Better Auth's `getBaseURL()` checks the `BETTER_AUTH_URL` environment variable before checking forwarded headers. If this env var is set (e.g., as a Fly.io secret), it takes precedence.
|
|
130
|
+
|
|
131
|
+
**Fix**: Either:
|
|
132
|
+
- Remove the `BETTER_AUTH_URL` env var entirely (risky — health checks need it)
|
|
133
|
+
- Use `allowedHosts` with `fallback: process.env.BETTER_AUTH_URL` — this bypasses the internal env var check while still having a fallback for non-proxied requests
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 8. OAuth callback hits backend directly (skips proxy)
|
|
138
|
+
|
|
139
|
+
**Symptom**: After Google OAuth, the browser lands on `https://backend.fly.dev/auth/callback/google` instead of going through the frontend proxy.
|
|
140
|
+
|
|
141
|
+
**Root cause**: Better Auth constructed the callback URL using the backend's origin instead of the frontend's. This means the forwarding headers weren't set correctly, or `allowedHosts` didn't match the frontend domain.
|
|
142
|
+
|
|
143
|
+
**Fix**:
|
|
144
|
+
1. Verify the proxy sets `X-Forwarded-Host: frontend.com` (not just strips headers)
|
|
145
|
+
2. Verify `allowedHosts` includes `frontend.com`
|
|
146
|
+
3. Use the curl verification command to check what `redirect_uri` is being generated
|
|
147
|
+
|
|
148
|
+
**Quick debug**: The most common cause is that the proxy strips forwarding headers but doesn't set new ones. The proxy must do both: strip (prevent spoofing) then set (tell backend the real origin).
|