fix(auth): add the account.issuer field better-auth 1.7 expects - #136
Merged
Conversation
Every email signup 500d with `The field "issuer" does not exist in the "account" Drizzle schema`. better-auth 1.7 added `account.issuer`, which namespaces an identity to the authority that issued it, and the drizzle adapter validates its models against our schema at request time — so a field we never regenerated surfaced as a runtime error, not a build one. Only new signups hit it, which is why it went unnoticed. `issuer` is the only drift. Checked by computing `getAuthTables()` for the installed version against every declared table, core and passkey plugin alike; the other 64 field assertions were already green. The column is NOT NULL rather than nullable because sign-in *filters* on it (`issuer = 'local:credential'` in the credential-account lookup), so a NULL issuer is an account nobody can log into. That makes the backfill load-bearing: drizzle-kit generated a bare `ADD COLUMN ... NOT NULL`, which fails outright on any populated table — verified against the dev DB, `column "issuer" of relation "account" contains null values`. Every existing install would have crash-looped on startup. 0009 instead adds nullable, backfills, then enforces NOT NULL, the same shape 0005 uses. Also stop both auth forms from collapsing a 500 into "Something went wrong. Please try again." — advice that can never work for a server-side fault. A 5xx now says it is a server problem and points at the logs, which is the only signal a self-hoster has. Regression coverage is a model-shape assertion driven by `auth.options`, so a plugin added later is checked without editing the test — more durable than an end-to-end signup test, and it fails at the exact field. Verified: signup 200, sign-in 200, wrong password 401, and the five pre-existing accounts backfilled to rows byte-identical to a freshly written one on every field sign-in matches against.
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.
Fixes #133.
Email signup returned 500 for every new account:
better-auth 1.7 added
account.issuer— it namespaces an identity to theauthority that issued it, so a provider ID can't collide across authentication
methods. The drizzle adapter validates its models against our schema at request
time, so a field we never regenerated showed up as a runtime 500 rather than a
build failure. Only new signups touched the model, which is why existing sessions
kept working and nobody noticed.
Is anything else drifted?
No —
issueris the only one. I computedgetAuthTables()for the installedbetter-auth (core models plus the passkey plugin's) and diffed it against every
declared table: 64 field assertions were already green, 2 failed, both on
account.issuer.Why NOT NULL, and why the backfill is load-bearing
issuerisn't just written, it's filtered on. Sign-in matchesaccount.issuer === 'local:credential', andfindCredentialAccount/updatePasswordput it in the WHERE clause. A NULL issuer is therefore anaccount nobody can log into — worth a database constraint, not just a
convention.
That makes the migration the risky part.
pnpm db:generateemitted:Run against the dev DB (in a rolled-back transaction):
Shipped as generated, that would crash-loop every existing self-hosted install
on container startup.
0009instead adds nullable → backfills → enforcesNOT NULL, the same shape0005already uses. Backfill islocal:credential,with a
CASEcovering a fork that added a social provider before upgrading.The migration is newly generated — no existing migration was hand-edited or
re-timestamped, and the journal diff is purely additive.
Error surfacing
Both auth forms collapsed every failure into "Something went wrong. Please try
again." — advice that can never work for a server-side fault, and the reason
#133 sat behind a dead end while the server was logging the exact cause. A 5xx
now says it's a server problem and points at the logs, which is the only signal
a self-hoster has. It deliberately does not echo the server's message on a 5xx.
Login had the identical swallow, so it got the same fix.
Regression test
src/db/schema/auth.test.tsasserts the whole expected model set against thetables we declare. It mirrors the adapter's real resolution —
schema[modelName][fieldName]by JS property key, not SQL column name — and is driven by
auth.options, so aplugin added to
src/lib/auth/index.tslater is covered without touching thetest. More durable than an end-to-end signup test, and it fails at the exact
field.
Verification
NOT NULL.sign-in matches against (
provider_id,issuer,account_id = user_id) — soexisting users are not locked out.
pnpm typecheckclean,pnpm lintclean,pnpm test1027 passed / 1 failed.The one failure is
investment-queries.test.ts > returns dayChange from holdings_history, which already fails on cleanmain— it pins hardcoded2026-05-09/2026-05-10dates against a now-relative window. Unrelated to thischange; worth its own issue.
🤖 Generated with Claude Code