brackt/BETTERAUTH_MIGRATION.md

184 lines
7 KiB
Markdown
Raw Permalink Normal View History

Migrate authentication from Clerk to BetterAuth (#324) * Migrate authentication from Clerk to BetterAuth (#322) Replaces @clerk/react-router with self-hosted better-auth to eliminate the external Clerk dependency and keep all user/session data in our own PostgreSQL database. **What changed** - New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler - New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param - New: UserMenu component replacing Clerk's UserButton - Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable - Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9) - All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin - root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query) - useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check - models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern) - Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix - scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts) - scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export - BETTERAUTH_MIGRATION.md: dev and production runbooks - All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server - Test fixtures: added emailVerified field **Follow-up (post-stable)** - Rename actor_clerk_id column → actor_user_id in commissioner_audit_log - Drop clerk_id column from users once migration confirmed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1. The adapter works correctly at runtime with our versions — the peer dep is only for stricter type checking. This unblocks npm ci in CI without a risky drizzle major-version upgrade. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:00:49 -07:00
# BetterAuth Migration Checklist
This document covers the manual steps required to complete the Clerk → BetterAuth migration in both development and production.
**What's automated** (runs via `scripts/migrate.mjs` on every deploy, idempotent):
- Schema migrations (sessions, accounts, verifications tables; nullable clerk_id; email_verified column)
- Convert `teams.owner_id` and `commissioners.user_id` from Clerk IDs → `users.id` UUIDs
- Mark all Clerk users as `email_verified = true`
- Create `accounts` records for Google and Discord OAuth users (via Clerk API)
**What's manual** (one-time steps you do by hand):
- Export user CSV from Clerk (for password hash migration)
- Run the password migration script
- Update OAuth redirect URIs in Google/Discord consoles
- Set env vars in production
---
## Development
### Step 1 — Add environment variables
Add these to your `.env` file:
```
BETTER_AUTH_SECRET="<random string run: openssl rand -base64 32>"
BETTER_AUTH_URL="http://localhost:5173"
GOOGLE_CLIENT_ID="<from Clerk dashboard Social Connections Google>"
GOOGLE_CLIENT_SECRET="<from Clerk dashboard Social Connections Google>"
DISCORD_CLIENT_ID="<from Clerk dashboard Social Connections Discord>"
DISCORD_CLIENT_SECRET="<from Clerk dashboard Social Connections Discord>"
# Keep temporarily for the automated OAuth migration (remove after confirmed working)
CLERK_SECRET_KEY="<your existing value>"
```
Remove once confirmed working: `VITE_CLERK_PUBLISHABLE_KEY`, `CLERK_WEBHOOK_SECRET`, `DEV_ADMIN_CLERK_ID`
### Step 2 — Apply the database schema migration
```bash
npm run db:migrate
```
Adds the `sessions`, `accounts`, and `verifications` tables; makes `clerk_id` nullable; adds `email_verified`.
### Step 3 — Add OAuth redirect URIs for localhost
In **Google Cloud Console** (APIs & Services → Credentials → your OAuth client):
- Add `http://localhost:5173/api/auth/callback/google` to Authorized redirect URIs
In **Discord Developer Portal** (your app → OAuth2):
- Add `http://localhost:5173/api/auth/callback/discord` to Redirects
### Step 4 — Run the automated data migration
```bash
node scripts/migrate.mjs
```
This converts owner IDs, creates Google/Discord OAuth accounts, and marks users as email-verified. It reads from the Clerk API using your `CLERK_SECRET_KEY`.
### Step 5 — Migrate email/password users
This requires exporting password hashes from Clerk (they're only in the CSV export, not the API).
1. Go to **Clerk dashboard → Users → Export** and download the CSV
2. Save it as `exported_users.csv` in the project root (it's gitignored)
3. Run:
```bash
node scripts/migrate-clerk-passwords.mjs
```
4. **Delete `exported_users.csv` immediately after** — it contains bcrypt password hashes
### Step 6 — Set yourself as admin
`DEV_ADMIN_CLERK_ID` no longer works. Set admin access directly in the database:
```sql
UPDATE users SET is_admin = true WHERE email = 'your@email.com';
```
### Step 7 — Verify sign-in works
- **Google / Discord**: sign in immediately — OAuth accounts were migrated automatically
- **Email/password**: sign in with the same password as before — bcrypt hashes were migrated
- Check that league ownership and commissioner access still works
After verifying, remove `CLERK_SECRET_KEY` from `.env`.
---
## Production
> Do Steps 13 before merging to main. The data migration (Step 4) and OAuth account creation run automatically on deploy.
### Step 1 — Add production environment variables
Add to your production environment (server docker-compose / secrets manager):
```
BETTER_AUTH_SECRET="<strong random string openssl rand -base64 32>"
BETTER_AUTH_URL="https://brackt.com"
GOOGLE_CLIENT_ID="<same as Clerk dashboard>"
GOOGLE_CLIENT_SECRET="<same as Clerk dashboard>"
DISCORD_CLIENT_ID="<same as Clerk dashboard>"
DISCORD_CLIENT_SECRET="<same as Clerk dashboard>"
# Keep temporarily — needed for the automated OAuth migration on first deploy
CLERK_SECRET_KEY="<your existing production value>"
```
### Step 2 — Add OAuth redirect URIs for production
In **Google Cloud Console**:
- Add `https://brackt.com/api/auth/callback/google` to Authorized redirect URIs
In **Discord Developer Portal**:
- Add `https://brackt.com/api/auth/callback/discord` to Redirects
### Step 3 — Migrate email/password users (before deploying)
The password migration runs against the production database directly — it does NOT go through Docker.
1. Go to **Clerk dashboard → Users → Export** and download the CSV for production users
2. Save as `exported_users.csv` in the project root (gitignored)
3. Run against the production database:
```bash
DATABASE_DIRECT_URL="<prod connection string>" CLERK_SECRET_KEY="<prod key>" node scripts/migrate-clerk-passwords.mjs
```
4. **Delete `exported_users.csv` immediately**
### Step 4 — Deploy
Merge to `main`. The GitHub Actions deploy will:
1. Build and push the Docker image
2. SSH to the server and run `docker compose up --no-deps migrate`
3. `scripts/migrate.mjs` runs — applies schema migrations, then (because `CLERK_SECRET_KEY` is set):
- Converts `teams.owner_id` and `commissioners.user_id` to `users.id` UUIDs
- Marks users as `email_verified`
- Creates `accounts` records for Google and Discord OAuth users
4. The app container starts with the new BetterAuth code
### Step 5 — Verify production sign-in
- Google and Discord users: sign in immediately
- Email/password users: sign in with the same password as before
- Spot-check that league ownership and commissioner access are intact
### Step 6 — Remove Clerk credentials
Once everything is confirmed, remove from the production environment:
```
CLERK_SECRET_KEY
VITE_CLERK_PUBLISHABLE_KEY
CLERK_WEBHOOK_SECRET
DEV_ADMIN_CLERK_ID
```
Delete the Clerk webhook endpoint from the Clerk dashboard (it no longer exists in the codebase).
On the next deploy, `scripts/migrate.mjs` will log `"CLERK_SECRET_KEY not set — skipping Clerk account migration"` and skip gracefully.
---
## Notes
### Why email/password migration is a separate manual step
Clerk's REST API does not expose password hashes — they're only available in the CSV dashboard export. The automated deploy script (`scripts/migrate.mjs`) handles OAuth accounts via the API, but password hashes must be exported manually and migrated with `scripts/migrate-clerk-passwords.mjs`.
The bcrypt hashes from Clerk are directly compatible with BetterAuth — no re-hashing is needed. Users sign in with the same password as before.
### What if a user signed up with both Google and email/password?
Both an OAuth account and a credential account will be created for them. BetterAuth handles this correctly — the user can sign in with either method.
### Future cleanup
After the migration is stable (a week or two after production deploy), open a follow-up PR to:
1. Remove the `clerk_id` column from `database/schema.ts` and run `npm run db:generate`
2. Remove `CLERK_SECRET_KEY` handling from `scripts/migrate.mjs`
3. Delete `scripts/migrate-clerk-passwords.mjs`