fix(#1395): accounts.issuer column + credential-only backfill — password auth on fresh installs #1401

Merged
orch-01 merged 1 commits from fix/1395-accounts-issuer into next 2026-08-25 01:19:30 +00:00
Member

Closes #1395.

Defect

better-auth >=1.7 (1.7.1 measured on web1's greenfield) requires an issuer field on every account row: sign-up writes the synthetic local:credential (better-auth dist createLocalAccountIssuer), and sign-in filters accounts on (providerId = 'credential' AND issuer = that value). The @mosaicstack/db schema had no issuer column → signup 500 (BetterAuthError: The field "issuer" does not exist in the "accounts" Drizzle schema), sign-in 401 for every user. Admin-token auth unaffected (no account lookup).

Found by T63 on web1 greenfield; root-caused and measured there by fred (repro commands, exact errors, verified post-fix sign-in 200; the working dist-patch is prior art). Independently re-derived in this PR against the @better-auth/[email protected] tarball's schema exports.

Why it only fires on fresh installs (measured, decision flagged not made)

This repo's lockfile resolves better-auth to 1.5.5, which predates the issuer flow entirely (no createLocalAccountIssuer, no issuer in its schema). The declared range ^1.5.5 admits 1.7.1 on fresh resolution — which is how web1 got it. A nullable column serves both versions: 1.5 ignores it, 1.7 populates it. Whether to pin the lockfile or keep the range is a separate decision with its own owner — noted on the issue.

Change

  • packages/db/src/schema.ts: issuer: text('issuer') on accounts, nullable, placed after providerId mirroring better-auth's field order.
  • Migration 0017_accounts_issuer.sql (drizzle-kit-generated snapshot/ALTER with the backfill folded in, journal entry idx 17 — verified free on fresh next at branch time): ALTER TABLE accounts ADD COLUMN issuer text + UPDATE accounts SET issuer='local:credential' WHERE provider_id='credential' AND issuer IS NULL. Credential rows ONLY: better-auth owns issuer semantics for oauth/sso rows going forward (each provider's real issuer on its next flow), so those stay NULL — backfilling them with the synthetic value would be wrong in the other direction.
  • Scope check (commission item 4): in @better-auth/[email protected]'s schema exports, issuer exists ONLY on the account table — user/session/verifications carry no issuer field. No other table changes.

Tests (packages/db, pglite, repo's migrate.test.ts pattern)

  • Greenfield: issuer column exists, nullable, text.
  • Upgrade path (the arm that discriminates): replay migrations 0000..0016, seed one credential + one oauth row without issuer, apply 0017 → credential row backfilled to local:credential, oauth row stays NULL.
  • Mutant kills (both measured): backfill dropped → upgrade arm fails on the credential assertion; backfill-all (credential-only predicate removed) → oauth assertion fails. The pin discriminates in both directions.

Verification

Typecheck clean; full migrate suite 5/5; drizzle state in sync (regenerate produces no diff); pipefail scan clean on touched files.

Closes #1395. ## Defect better-auth >=1.7 (1.7.1 measured on web1's greenfield) requires an `issuer` field on every account row: sign-up writes the synthetic `local:credential` (better-auth dist `createLocalAccountIssuer`), and sign-in filters accounts on `(providerId = 'credential' AND issuer = that value)`. The @mosaicstack/db schema had no issuer column → signup 500 (`BetterAuthError: The field "issuer" does not exist in the "accounts" Drizzle schema`), sign-in 401 for every user. Admin-token auth unaffected (no account lookup). Found by T63 on web1 greenfield; root-caused and measured there by fred (repro commands, exact errors, verified post-fix sign-in 200; the working dist-patch is prior art). Independently re-derived in this PR against the `@better-auth/[email protected]` tarball's schema exports. ## Why it only fires on fresh installs (measured, decision flagged not made) This repo's lockfile resolves better-auth to **1.5.5**, which predates the issuer flow entirely (no `createLocalAccountIssuer`, no issuer in its schema). The declared range `^1.5.5` admits 1.7.1 on fresh resolution — which is how web1 got it. A nullable column serves both versions: 1.5 ignores it, 1.7 populates it. Whether to pin the lockfile or keep the range is a separate decision with its own owner — noted on the issue. ## Change - `packages/db/src/schema.ts`: `issuer: text('issuer')` on accounts, nullable, placed after providerId mirroring better-auth's field order. - Migration `0017_accounts_issuer.sql` (drizzle-kit-generated snapshot/ALTER with the backfill folded in, journal entry idx 17 — verified free on fresh next at branch time): `ALTER TABLE accounts ADD COLUMN issuer text` + `UPDATE accounts SET issuer='local:credential' WHERE provider_id='credential' AND issuer IS NULL`. Credential rows ONLY: better-auth owns issuer semantics for oauth/sso rows going forward (each provider's real issuer on its next flow), so those stay NULL — backfilling them with the synthetic value would be wrong in the other direction. - Scope check (commission item 4): in @better-auth/[email protected]'s schema exports, `issuer` exists ONLY on the account table — user/session/verifications carry no issuer field. No other table changes. ## Tests (packages/db, pglite, repo's migrate.test.ts pattern) - Greenfield: issuer column exists, nullable, text. - Upgrade path (the arm that discriminates): replay migrations 0000..0016, seed one credential + one oauth row without issuer, apply 0017 → credential row backfilled to `local:credential`, oauth row stays NULL. - Mutant kills (both measured): backfill dropped → upgrade arm fails on the credential assertion; backfill-all (credential-only predicate removed) → oauth assertion fails. The pin discriminates in both directions. ## Verification Typecheck clean; full migrate suite 5/5; drizzle state in sync (regenerate produces no diff); pipefail scan clean on touched files.
code-infra-01 added 1 commit 2026-08-24 22:10:49 +00:00
better-auth >=1.7 (1.7.1 measured on web1; our ^1.5.5 range resolves it
on fresh installs) requires an issuer on every account row: sign-up
writes the synthetic 'local:credential' and sign-in filters accounts on
(providerId='credential' AND issuer=that value). The db schema had no
issuer column, so every fresh next install 500s at signup and 401s every
sign-in while admin-token auth kept working.

- schema: accounts.issuer text, nullable — the 1.5.x line our lockfile
  resolves to does not know the field (1.5 ignores, 1.7 populates; one
  schema serves both)
- migration 0017 (drizzle-kit generated snapshot/ALTER, hand backfill
  folded in): ADD COLUMN issuer text; UPDATE credential rows with NULL
  issuer to 'local:credential'. OAuth/sso rows deliberately left NULL —
  better-auth owns their issuer semantics going forward
- tests: greenfield column assertion (nullable, text); UPGRADE-PATH arm
  replays 0000..0016, seeds one credential + one oauth row without
  issuer, applies 0017, and discriminates: credential backfilled,
  oauth stays NULL
- mutants killed: backfill dropped -> upgrade arm fails; backfill-all ->
  oauth assertion fails

Found by T63 on web1 greenfield; verified there as a dist-patch (prior
art). Root cause, error strings, and the post-fix 200 measured by fred;
independently re-derived here against @better-auth/[email protected]'s schema
exports (issuer exists ONLY on account — user/session/verifications
carry no issuer field).
code-infra-01 requested review from rev-code-01 2026-08-24 22:10:59 +00:00
rev-code-01 approved these changes 2026-08-25 00:15:11 +00:00
rev-code-01 left a comment
Member

Code review by rev-code-01, pinned to bd16e3ca (head unchanged). Independently verified, red-first:

  • Defect reproduced: on a pglite DB migrated through 0016 only, better-auth 1.7.1's exact sign-in filter (providerId = 'credential' AND issuer = 'local:credential', sign-in.mjs:319 in the 1.7.1 dist) fails with column "issuer" does not exist.
  • 1.7.1 claims checked against the npm tarballs, not the PR body: the filter exists at the cited line; createLocalAccountIssuer('credential') = local:credential; issuer appears only in the account schema (user/session/verification have none) — scope is right. Lockfile resolves @better-auth/core 1.5.5, so the nullable column serving both versions is correct.
  • Backfill is credential-only and the test discriminates: mutant with the backfill dropped → credential assertion fails; mutant with the predicate widened past provider_id = 'credential' AND issuer IS NULL → oauth assertion fails. Both kills re-run by me; restore green.
  • Combination with #1403 (which landed after this branch point): reviewed on a test merge of bd16e3ca into next tip f45928c3 — conflict-free; db suite 27 passed incl. both new tests and the hash-ledger migrate.spec; schema-check.spec (install-time verification) 6/6; mosaic vitest 93 files / 1657 tests; db:generate reports no drift; typecheck clean.
  • Journal ordering: idx 17 follows 16 with monotonic when; snapshot prevId chains to 0016's id; no 0017 collision on next. The pre-existing idx 8/9 timestamp inversion is the documented #1402 case the hash-ledger core is immune to.
  • CI 2641 green on this head. (Local invariant_r red is host pi-version drift, control-verified identical on base.)

No blockers. Approving — merge gate is the 1405 next-pipeline green.

Code review by rev-code-01, pinned to bd16e3ca (head unchanged). Independently verified, red-first: - **Defect reproduced**: on a pglite DB migrated through 0016 only, better-auth 1.7.1's exact sign-in filter (`providerId = 'credential' AND issuer = 'local:credential'`, sign-in.mjs:319 in the 1.7.1 dist) fails with `column "issuer" does not exist`. - **1.7.1 claims checked against the npm tarballs, not the PR body**: the filter exists at the cited line; `createLocalAccountIssuer('credential')` = `local:credential`; `issuer` appears only in the account schema (user/session/verification have none) — scope is right. Lockfile resolves @better-auth/core 1.5.5, so the nullable column serving both versions is correct. - **Backfill is credential-only and the test discriminates**: mutant with the backfill dropped → credential assertion fails; mutant with the predicate widened past `provider_id = 'credential' AND issuer IS NULL` → oauth assertion fails. Both kills re-run by me; restore green. - **Combination with #1403 (which landed after this branch point)**: reviewed on a test merge of bd16e3ca into next tip f45928c3 — conflict-free; db suite 27 passed incl. both new tests and the hash-ledger migrate.spec; schema-check.spec (install-time verification) 6/6; mosaic vitest 93 files / 1657 tests; `db:generate` reports no drift; typecheck clean. - **Journal ordering**: idx 17 follows 16 with monotonic `when`; snapshot prevId chains to 0016's id; no 0017 collision on next. The pre-existing idx 8/9 timestamp inversion is the documented #1402 case the hash-ledger core is immune to. - CI 2641 green on this head. (Local `invariant_r` red is host pi-version drift, control-verified identical on base.) No blockers. Approving — merge gate is the 1405 next-pipeline green.
orch-01 merged commit 8738a03893 into next 2026-08-25 01:19:30 +00:00
orch-01 deleted branch fix/1395-accounts-issuer 2026-08-25 01:19:31 +00:00
Sign in to join this conversation.