chore: upgrade Better Auth 1.7, unify deploys, guard db:push - #2181
Merged
Conversation
koistya
force-pushed
the
chore/better-auth-1.7-release-guards
branch
from
August 23, 2026 20:25
964e27d to
b61bf8d
Compare
Better Auth 1.7 keys an external identity on `(issuer, accountId)` instead
of `(providerId, accountId)`, and `issuer` is required. `identity` gains the
column and swaps its composite unique; the old constraint is now implied,
since issuer is a function of providerId. `db/migrations/0000_init.sql` is
regenerated in place per the squashed-init convention - run `bun db:push`
after pulling.
The snapshot's top-level `version` moves from 1 to 7, the format current
drizzle-kit writes. It was hand-edited to 1 at some point, and drizzle-kit
had been rejecting it with `data is malformed`, so `bun db:generate` did
nothing on any schema change. The journal entry's own `version` is not read.
`getAuthTables()` also started reporting the indexes, defaults and cascades
a plugin declares. `generate-auth-schema.ts` emitted none of them, and the
validation command told reviewers to ignore exactly the metadata that
changed, so both now carry it.
Three rules the docs stated become controls the code enforces:
- `bun deploy:{staging,production}` builds and deploys api, app and web in
one script that `deploy.yml` also calls, so a release from a laptop and
one from Actions cannot drift in order or environment selection. It owns
the production-to-empty-`--env` mapping and rejects any other name.
- `db:push` refuses a non-local database. Reshaping in place is a migration
nobody reviewed, and it drops columns to make the shapes agree.
`ALLOW_REMOTE_DB_PUSH=1` is the deliberate way past it.
- `grant-app-role.sql` provisions the least-privilege role Hyperdrive should
use. It asserts the runner owns the database and schema first, because
Postgres answers an unauthorised REVOKE with a warning and exit 0.
The four `test` scripts become `bun --bun vitest`. Bun 1.4 runs Vitest as a
host runtime, and only the runtime changes - same Vitest, same config, same
92 tests, consistently faster. Vitest itself stays: Bun's own runner has no
`environment: happy-dom`, no `projects`, and no `vi.*` namespace, so the DOM
suite cannot run under it.
Coverage is wired up per the Vitest guide, replacing an `apps/app` script
that could not run: no provider was installed and it asked for watch mode.
`coverage.include` is what makes an unimported module report 0% rather than
vanish from the total. It runs on Node, the one place the Bun runtime does
not hold up - merging v8 coverage for this suite overflows its stack.
`no-unused-vars` gains `ignoreRestSiblings`, so `const { password, ...rest }`
stops being a lint error in a kit that ships a password column.
koistya
force-pushed
the
chore/better-auth-1.7-release-guards
branch
from
August 23, 2026 20:47
b61bf8d to
aa19a3f
Compare
stanleynue
approved these changes
Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Better Auth 1.7 changes how an external identity is keyed, which is a required schema migration rather than a version bump. Working through it surfaced three rules this repo already documented but did not enforce — how a release is deployed, where
db:pushmay point, and which database role the workers run as — so each becomes a control the code applies.Better Auth 1.7
An identity is now keyed on
(issuer, accountId)instead of(providerId, accountId), andissueris required.identitygains the column and swaps its composite unique. The old constraint is not kept: issuer is a function of providerId, so(issuer, accountId)implies it.db/migrations/0000_init.sqlis regenerated in place per the squashed-init convention — runbun db:pushafter pulling.getAuthTables()also began reporting the indexes, literal defaults and FK cascades a plugin declares.db/scripts/generate-auth-schema.tsemitted none of them, and/validate-auth-schematold reviewers to ignore exactly the metadata that changed, so both now carry it. Everything else — Stripe'sorganization.enabled, the account selectors,getIp→getIP— was already satisfied or unused.bun db:generateworks againThe snapshot's top-level
versionwas hand-edited to"1"at some point, and current drizzle-kit rejects that withdata is malformed— sodb:generatesilently did nothing on any schema change. It moves to"7", the format drizzle-kit writes today, and now reportsNo schema changesagainst this branch's schema. The journal entry's ownversionis never read, so it stays as is.One deploy path
bun deploy:{staging,production}builds, checks eachdist/exists, then deploysapi→app→web.deploy.ymlcalls the same script with--skip-build, replacing three inlinewrangler deploylines, so a release from a laptop and one from Actions cannot drift in order or in environment selection. The script owns the production → empty---envmapping and rejects any other name, so a value lost in transit fails the run instead of deploying production.No
--env-fileflags: Wrangler already loads.envand.env.localfor every command and merges them underprocess.env, so listing them would only suppress.env.staging.localon a staging deploy without changing which credentials win.db:pushrefuses a non-local databasepushinfers a schema change and applies it in place — against real rows that is a migration nobody reviewed, and it drops a column, and its data, to make the shapes agree. The rule lived indocs/security/checklist.md;db/scripts/guard-push.tsnow enforces it, withALLOW_REMOTE_DB_PUSH=1as the deliberate way past. The classifier is tested in both directions, because a local database wrongly refused teaches people to reach for the override, and an override reached for by habit is no longer a control.Least-privilege database role
db/scripts/grant-app-role.sqlprovisions the role Hyperdrive should hold: DML only, no DDL,CONNECTrevoked fromPUBLICso a leaked staging credential cannot open production on the same Neon project. It refuses to run unless the caller owns the database and schemapublic, because Postgres answers an unauthorisedREVOKEwith a warning and exit 0 — without the check, the wrong runner leaves a role that looks provisioned with none of the boundary around it. It also refuses a role name that already carries privileges of its own, since it only ever grants.Tests run on Bun, coverage works
The four
testscripts becomebun --bun vitest. Bun 1.4 runs Vitest as a host runtime; only the runtime changes — same Vitest, same config, same 92 tests, consistently faster across every documented invocation (--run,--project, filename filters, watch).Vitest itself stays. Bun's own runner has no
environment: happy-dom, noprojects, and novi.*namespace —bun test apps/app/lib/theme.test.tsxfails withdocument is not defined, and the suite usesvi.mock,vi.hoistedandvi.stubGlobal. Bun's--pool=threadswas also measured and is 2× slower here, so the defaultforksstays.Coverage is set up per the Vitest guide, replacing an
apps/appscript that could not run — no provider was installed, and it asked for watch mode.coverage.includeis what makes an unimported module report 0% rather than vanish from the total; only generated code, build output and test scaffolding are excluded. It runs on Node, the one place the Bun runtime does not hold up: merging v8 coverage for this suite overflows the stack inside@bcoe/v8-coverage, reproducibly.Updating a project built on this kit
Forks track this repo as the
seedremote and sync with themerge-seedskill –/merge-seedin Claude Code, ormerge-seedin Codex, both reading.agents/skills/merge-seed/SKILL.md. It branches, mergesseed/mainwithzdiff3so the merge base stays visible, and resolves under one rule: upstream owns mechanism, your project owns identity and scope.Two things here need a decision the skill deliberately will not make for you.
It will stop on the migration.
0000_init.sqlis rewritten in place under this repo's squashed-init convention, and the skill treats a migration that may already have run as immutable, so it escalates instead of resolving. That is the right outcome: keep your own migration history, take only thedb/schema/change, and letbun db:generatewriteissueras a new migration for your project. That command works again as of this PR.A live database needs a backfill.
issuerisNOT NULLwith no default, so the generated migration will fail on a non-emptyidentitytable – Drizzle emits a singleADD COLUMN ... NOT NULL. Split it by hand: add the column nullable, populate it, then add the constraint.Backfill outline
Issuer values come from Better Auth's 1.7 upgrade guide:
local:credentialfor password accounts, the provider's published issuer for OIDC providers, andlocal:oauth:<providerId>for an OAuth provider that publishes none.Credential accounts key on the linked user's
id, which is already whataccount_idholds – no change there. Stop and reconcile by hand if the collision query returns anything; never merge users by matching email.Everything else in this PR is ordinary mechanism the skill adopts on its own. Verify with its checklist afterwards:
Also
@typescript-eslint/no-unused-varsgainsignoreRestSiblings, soconst { password, ...rest } = userstops being a lint error in a kit that ships apasswordcolumn.Verification
bun run test --runbun run coveragebun typecheckbun run build,bun run docs:buildbun db:generateNo schema changeswrangler deploy --dry-run(all three workers,--env staging)bun linton changed filesThe Better Auth flows were exercised end to end against PGlite — sign-up writes
issuer: "local:credential", plus email OTP, organization create, invite, and the session hook.grant-app-role.sqlwas verified against a real PostgreSQL 17 cluster: DML works,CREATE/TEMP/DROP/ALTERare denied, cross-databaseCONNECTis denied, and the wrong-runner and elevated-role names exit non-zero without creating anything.