Skip to content

Commit 0aa7603

Browse files
authored
chore: upgrade Better Auth 1.7, unify deploys, guard db:push (#2181)
Better Auth 1.7 rekeys external identities, which is a required schema migration rather than a version bump. Working through it surfaced three rules the docs stated but nothing enforced — how a release deploys, where `db:push` may point, and which database role the workers run as — so each becomes a control. New: scripts/deploy.ts (release path), db/scripts/guard-push.ts plus local-database.ts (push guard), db/scripts/grant-app-role.sql (least-privilege role), coverage config in vitest.config.ts. Invariants to preserve when editing: - An identity is keyed on `(issuer, accountId)`, never on `providerId`, which names only the local provider configuration. Do not restore `identity_provider_account_unique`: issuer is a function of providerId, so the new constraint implies it. - `0000_init.sql` is regenerated in place under the squashed-init convention, never appended to. `0000_snapshot.json` must keep top-level `"version": "7"` — at `"1"` drizzle-kit rejects it, `db:generate` reports `data is malformed` and writes nothing, and `db:check` exits non-zero. The journal entry's own `version` is not read. - `scripts/deploy.ts` is the only deploy path; deploy.yml calls it with `--skip-build`. Production is Wrangler's empty `--env` and the script owns that mapping, so do not inline `wrangler deploy` back into the workflow or spell the environment out twice. - deploy.ts passes no `--env-file`. Wrangler already loads `.env` and `.env.local` from the cwd and merges them under `process.env`; listing them explicitly only suppresses `.env.<env>.local` on a staging deploy. - `db:push` runs guard-push.ts first and refuses any non-loopback host. `ALLOW_REMOTE_DB_PUSH=1` is the sole bypass; widening the classifier in local-database.ts trains people to reach for it. - grant-app-role.sql asserts database and `public` ownership before it creates anything, because Postgres answers an unauthorised REVOKE with a warning and exit 0. Removing the `DO` block makes a wrong-runner invocation report success with none of the boundary built. - Tests run Vitest on Bun (`bun --bun vitest`); `coverage` runs it on Node. The inconsistency is deliberate — merging this suite's v8 coverage overflows Bun's stack inside `@bcoe/v8-coverage`. - `coverage.include` must keep listing source globs. Without it an unimported module is absent from the total instead of reporting 0%, and excluding untested source to raise the number defeats the point. - generate-auth-schema.ts must keep emitting table `indexes` and per-field `index`, `defaultValue` and `references.onDelete`. 1.7 moved the account identity key into exactly that metadata, and `/validate-auth-schema` reads this output as its source of truth. Fixes the defect #2179 logged and left: `bun db:check` now passes. Also adds `ignoreRestSiblings` to `no-unused-vars`, so `const { password, ...rest }` is no longer an error. Fresh clones run `bun db:push` after pulling. Forks syncing with `/merge-seed` will see it escalate on `0000_init.sql`, which is correct: keep local migration history and add `issuer` as a new migration with a backfill, since the column is NOT NULL with no default. docs/testing.md and docs/database/ carry the details.
1 parent aceeb4f commit 0aa7603

38 files changed

Lines changed: 1134 additions & 517 deletions

.claude/commands/validate-auth-schema.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ Check that the Drizzle schema in `db/schema/` still satisfies what Better Auth e
66
bun run db/scripts/generate-auth-schema.ts
77
```
88

9-
The script builds a real auth instance from `apps/api/lib/auth.ts` with fixed placeholder credentials, every optional integration switched on, so the output is the full set of tables rather than the subset your environment happens to enable. Per field it carries the `type`, the `required` and `unique` flags, and the reference target — nothing else.
9+
The script builds a real auth instance from `apps/api/lib/auth.ts` with fixed placeholder credentials, every optional integration switched on, so the output is the full set of tables rather than the subset your environment happens to enable. Per field it carries the `type`, the `required`, `unique` and `index` flags, any literal `defaultValue`, and the reference target with its `onDelete`. Per table it carries `indexes`: the composite indexes a plugin declares, which is how the account identity key `(issuer, accountId)` arrives.
1010

1111
Compare that output against `db/schema/` and report:
1212

1313
- Tables Better Auth expects that are missing, and tables no plugin in `auth.ts` needs any more.
1414
- Field-level drift: missing or extra columns, mismatched types, and `required` or `unique` flags that disagree.
1515
- Foreign keys pointing at the wrong table or column.
16-
- Defaults, `onDelete`, and indexes are **not** in the generated output — they are this project's decisions. Check those against the conventions in `db/AGENTS.md` instead, and do not report them as Better Auth drift.
16+
- A `true` `index` flag, a table-level entry in `indexes`, a literal `defaultValue`, or an `onDelete` in the output is Better Auth asking for it: report a schema that lacks one. Indexes, defaults and cascades the project adds beyond that list are its own decisions — check those against the conventions in `db/AGENTS.md` and do not report them as drift.
1717
- Local naming that must stay mapped, not renamed away: Better Auth's `account` is our `identity` table.
1818
- Project-specific columns, indexes, and constraints are expected and fine on their own. Flag any that constrain a write Better Auth performs: a `NOT NULL` column with no database default breaks every insert into that table, and an extra unique or check constraint can reject a row Better Auth considers valid. Fields that must participate in its models belong in `additionalFields` rather than added to the table behind its back.
1919

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Before starting work on a significant change, open an issue to discuss your prop
1616

1717
### Prerequisites
1818

19-
- [Bun](https://bun.sh) >= 1.3.0
19+
- [Bun](https://bun.sh) >= 1.4.0
2020
- [Node.js](https://nodejs.org) >= 20 (for some tooling)
2121
- [Git](https://git-scm.com)
2222

.github/workflows/deploy.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ jobs:
6464
# `apps/email/dist` is not deployed directly – the API worker imports
6565
# `@repo/email`, whose exports point there, so `worker.ts` fails to bundle
6666
# without it.
67+
#
68+
# `scripts/deploy.ts` checks the same three, and this stays anyway: that
69+
# check runs after the migration, and a restore that silently dropped a
70+
# `dist` should fail before the point of no return, not after it.
6771
- name: Verify build artifacts
6872
run: |
6973
test -d apps/email/dist
@@ -168,14 +172,10 @@ jobs:
168172
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
169173
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
170174
DEPLOY_ENV: ${{ inputs.environment }}
171-
run: |
172-
# Recomputed rather than carried from the preflight: an empty `--env`
173-
# selects production, so a value that went missing would deploy it.
174-
if [[ "$DEPLOY_ENV" == "production" ]]; then
175-
env_args=(--env "")
176-
else
177-
env_args=(--env "$DEPLOY_ENV")
178-
fi
179-
bun wrangler deploy --config apps/api/wrangler.jsonc "${env_args[@]}"
180-
bun wrangler deploy --config apps/app/wrangler.jsonc "${env_args[@]}"
181-
bun wrangler deploy --config apps/web/wrangler.jsonc "${env_args[@]}"
175+
# `bun deploy:{staging,production}` runs the same script locally, so a
176+
# release from a laptop and one from CI cannot drift in order or in
177+
# environment selection. It maps production to Wrangler's empty `--env`
178+
# itself and rejects any other environment name outright, so a value
179+
# lost in transit fails the run rather than deploying production.
180+
# `--skip-build` because the restored artifact is the verified build.
181+
run: bun scripts/deploy.ts "$DEPLOY_ENV" --skip-build

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## Tech Stack
1414

15-
- **Toolchain:** Bun >=1.3.0 (package manager, scripts, local API server), TypeScript 6.0, ESM (`"type": "module"`)
15+
- **Toolchain:** Bun >=1.4.0 (package manager, scripts, local API server), TypeScript 6.0, ESM (`"type": "module"`)
1616
- **Production runtime:** Cloudflare Workers (workerd) – not Bun. Code that ships must run on Workers APIs.
1717
- **Frontend:** React 19, TanStack Router, TanStack Query, Jotai, shadcn/ui (new-york), Tailwind CSS v4
1818
- **Backend:** Hono, tRPC 11, Better Auth (email OTP, passkey, Google OAuth, organizations)
@@ -31,11 +31,12 @@ bun lint # ESLint with cache
3131
bun typecheck # tsc --build (builds apps/email for its types)
3232
bun infra:check # Terraform fmt + validate, no credentials or state
3333
bun ui:add <component> # Add shadcn/ui component to packages/ui
34+
bun deploy:{staging,production} # Build and deploy api → app → web; no migrations
3435

3536
# Per-app: bun {web,app,api}:{dev,build,deploy}; test for app/api, check for web
3637
# Database: bun db:{push,generate,migrate,studio,seed,export}
3738
# :staging / :production on migrate, studio, export; seed stops at :staging;
38-
# push and generate are local-only
39+
# generate is local-only, and push refuses a non-local database
3940
```
4041

4142
## Verification

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Each app deploys independently to Cloudflare Workers. The web worker routes `/ap
5454

5555
## Prerequisites
5656

57-
- [Bun](https://bun.sh/) v1.3+ (replaces Node.js and npm)
57+
- [Bun](https://bun.sh/) v1.4+ (replaces Node.js and npm)
5858
- [VS Code](https://code.visualstudio.com/) with our [recommended extensions](.vscode/extensions.json)
5959
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) browser extension (recommended)
6060
- [Cloudflare account](https://dash.cloudflare.com/sign-up) for deployment
@@ -172,15 +172,13 @@ bun db:migrate:production # reads .env.production.local, and only that file
172172
### 4. Build and Deploy
173173

174174
```bash
175-
# Build every deployable workspace; Bun orders workspace dependencies
176-
bun run build # Build all deployable workspaces
177-
178-
# Deploy service-binding targets before the web router
179-
bun api:deploy --env=""
180-
bun app:deploy --env=""
181-
bun web:deploy --env=""
175+
# Builds, then deploys api -> app -> web (service-binding targets first)
176+
bun deploy:staging
177+
bun deploy:production
182178
```
183179

180+
CI runs the same script, so a release from your machine and one from GitHub Actions cannot drift.
181+
184182
## Backers
185183

186184
<a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>

apps/api/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,36 @@
1212
"predev": "bun --filter @repo/email build",
1313
"dev": "bun run --watch --env-file ../../.env --env-file ../../.env.local ./dev.ts",
1414
"build": "bun build index.ts --outdir dist --target bun",
15-
"test": "vitest",
15+
"test": "bun --bun vitest",
1616
"typecheck": "tsc --noEmit",
1717
"deploy": "wrangler deploy --env-file ../../.env --env-file ../../.env.local",
1818
"logs": "wrangler tail --env-file ../../.env --env-file ../../.env.local"
1919
},
2020
"dependencies": {
21-
"@better-auth/passkey": "^1.6.29",
22-
"@better-auth/stripe": "^1.6.29",
21+
"@better-auth/passkey": "^1.7.1",
22+
"@better-auth/stripe": "^1.7.1",
2323
"@repo/core": "workspace:*",
2424
"@repo/db": "workspace:*",
2525
"@repo/email": "workspace:*",
2626
"@trpc/server": "^11.18.0",
27-
"better-auth": "^1.6.29",
27+
"better-auth": "^1.7.1",
2828
"drizzle-orm": "^0.45.2",
2929
"postgres": "^3.4.9",
30-
"resend": "^6.20.0",
30+
"resend": "^6.22.0",
3131
"stripe": "^22.5.0"
3232
},
3333
"peerDependencies": {
34-
"hono": "^4.13.2",
34+
"hono": "^4.13.3",
3535
"zod": "^4.4.3"
3636
},
3737
"devDependencies": {
38-
"@cloudflare/workers-types": "^5.20260816.1",
38+
"@cloudflare/workers-types": "^5.20260823.1",
3939
"@repo/typescript-config": "workspace:*",
40-
"@types/bun": "^1.3.14",
41-
"hono": "^4.13.2",
40+
"@types/bun": "^1.4.0",
41+
"hono": "^4.13.3",
4242
"typescript": "~6.0.3",
43-
"vitest": "~4.1.10",
44-
"wrangler": "^4.123.0",
43+
"vitest": "~4.1.11",
44+
"wrangler": "^4.125.0",
4545
"zod": "^4.4.3"
4646
}
4747
}

apps/app/package.json

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,21 @@
77
"dev": "vite serve",
88
"build": "vite build",
99
"preview": "vite preview",
10-
"test": "vitest",
11-
"coverage": "vitest --coverage",
10+
"test": "bun --bun vitest",
1211
"typecheck": "tsc --noEmit",
1312
"deploy": "wrangler deploy --env-file ../../.env --env-file ../../.env.local",
1413
"logs": "wrangler tail --env-file ../../.env --env-file ../../.env.local"
1514
},
1615
"dependencies": {
17-
"@better-auth/passkey": "^1.6.29",
18-
"@better-auth/stripe": "^1.6.29",
16+
"@better-auth/passkey": "^1.7.1",
17+
"@better-auth/stripe": "^1.7.1",
1918
"@repo/ui": "workspace:*",
20-
"@tanstack/react-query": "^5.101.4",
21-
"@tanstack/react-router": "^1.170.29",
19+
"@tanstack/react-query": "^5.102.2",
20+
"@tanstack/react-router": "^1.170.32",
2221
"@trpc/client": "^11.18.0",
23-
"better-auth": "^1.6.29",
22+
"better-auth": "^1.7.1",
2423
"jotai": "^2.20.2",
25-
"lucide-react": "^1.31.0",
24+
"lucide-react": "^1.33.0",
2625
"react": "^19.2.8",
2726
"react-dom": "^19.2.8",
2827
"react-error-boundary": "^6.1.3",
@@ -32,24 +31,24 @@
3231
"@repo/api": "workspace:*",
3332
"@repo/typescript-config": "workspace:*",
3433
"@tailwindcss/vite": "^4.3.3",
35-
"@tanstack/react-devtools": "^0.10.10",
36-
"@tanstack/react-query-devtools": "^5.101.4",
34+
"@tanstack/react-devtools": "^0.10.12",
35+
"@tanstack/react-query-devtools": "^5.102.2",
3736
"@tanstack/react-router-devtools": "^1.167.1",
38-
"@tanstack/router-plugin": "^1.168.32",
37+
"@tanstack/router-plugin": "^1.168.35",
3938
"@testing-library/jest-dom": "^7.0.1",
4039
"@testing-library/react": "^16.3.2",
41-
"@testing-library/user-event": "^14.6.4",
42-
"@types/bun": "^1.3.14",
40+
"@testing-library/user-event": "^14.6.6",
41+
"@types/bun": "^1.4.0",
4342
"@types/node": "^26.2.0",
4443
"@types/react": "^19.2.18",
4544
"@types/react-dom": "^19.2.4",
46-
"@vitejs/plugin-react": "^6.0.5",
47-
"happy-dom": "^20.11.2",
45+
"@vitejs/plugin-react": "^6.1.0",
46+
"happy-dom": "^20.11.6",
4847
"tailwindcss": "^4.3.3",
4948
"tw-animate-css": "^1.4.0",
5049
"typescript": "~6.0.3",
51-
"vite": "~8.2.1",
52-
"vitest": "~4.1.10",
53-
"wrangler": "^4.123.0"
50+
"vite": "~8.2.2",
51+
"vitest": "~4.1.11",
52+
"wrangler": "^4.125.0"
5453
}
5554
}

apps/web/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@
1313
"logs": "wrangler tail --env-file ../../.env --env-file ../../.env.local"
1414
},
1515
"dependencies": {
16-
"@astrojs/react": "^6.0.2",
16+
"@astrojs/react": "^6.0.4",
1717
"@repo/ui": "workspace:*",
18-
"astro": "^7.2.2",
19-
"hono": "^4.13.2",
18+
"astro": "^7.2.4",
19+
"hono": "^4.13.3",
2020
"react": "^19.2.8",
2121
"react-dom": "^19.2.8"
2222
},
2323
"devDependencies": {
2424
"@astrojs/check": "^0.9.10",
25-
"@cloudflare/workers-types": "^5.20260816.1",
25+
"@cloudflare/workers-types": "^5.20260823.1",
2626
"@repo/typescript-config": "workspace:*",
2727
"@tailwindcss/vite": "^4.3.3",
2828
"@types/react": "^19.2.18",
2929
"@types/react-dom": "^19.2.4",
3030
"tailwindcss": "^4.3.3",
3131
"typescript": "~6.0.3",
32-
"wrangler": "^4.123.0"
32+
"wrangler": "^4.125.0"
3333
}
3434
}

0 commit comments

Comments
 (0)