Observation-class finding, noticed while executing #7994 (PR #8121) on the sibling gate. Nothing is miscovered today — this is about a gate that would stay silent tomorrow.
The asymmetry
packages/plugins/plugin-auth/src/ holds two gates that both derive from getAuthTables() and ask opposite questions:
| gate | question | plugin list | reconciled against auth-manager.ts? |
|---|
managed-extension-fields.test.ts (D7) | is an extension field colliding with a column better-auth owns? | AUTH_MANAGER_PLUGINS, 14 factories | yes — authManagerPluginFactories() scans auth-manager.ts for its await import(...) sites and fails on any factory not accounted for |
better-auth-schema-parity.test.ts (#3624) | is every column better-auth writes provisioned in platform-objects? | a literal array of 6 — organization, twoFactor, admin, phoneNumber, jwt, deviceAuthorization | no — nothing reads auth-manager.ts |
#7820 gave the D7 gate its tripwire on the reasoning that "a plugin nobody loaded owns columns nobody compared". That reasoning applies verbatim to the parity gate, which never got one.
Why it is currently harmless, and why that is the fragile part
The six cover today's model-contributing plugins. Of the other eight the auth manager can assemble, sso / scim / oauthProvider have dedicated blocks or their own parity test, and bearer / haveIBeenPwned / magicLink / genericOAuth / customSession contribute no model surface at all right now.
So the gap is entirely prospective, and it opens silently in two ways:
- A plugin added to
auth-manager.ts. D7 goes red until someone accounts for it; the parity gate says nothing and keeps passing. - A version bump giving one of the five no-model plugins a column. Same silence.
Either is the #3624 failure shape — an unprovisioned column better-auth writes, which surfaces as a runtime 500 (team.memberCount) rather than as a red gate.
Suggested direction (not a decision)
The cheapest fix is the one already proven in-repo: give the parity gate the same accounting-map-plus-scan shape as AUTH_MANAGER_PLUGINS, where an entry either carries a construct thunk (so declaring coverage and delivering it cannot come apart) or a written skip reason. Note the two gates deliberately call getAuthTables() differently — parity passes the schema: options from auth-schema-config.ts, D7 must not — so this is a shared reconciliation, not a shared plugin set. Worth checking whether the accounting map can be extracted and consumed by both without merging the two calls.
Filed unassigned for triage. Backlinks: #7994 / PR #8121 (the D7 coverage expansion that surfaced it) - #7820 (where the D7 tripwire came from) - #3624 (the original parity hole).
Observation-class finding, noticed while executing #7994 (PR #8121) on the sibling gate. Nothing is miscovered today — this is about a gate that would stay silent tomorrow.
The asymmetry
packages/plugins/plugin-auth/src/holds two gates that both derive fromgetAuthTables()and ask opposite questions:auth-manager.ts?managed-extension-fields.test.ts(D7)AUTH_MANAGER_PLUGINS, 14 factoriesauthManagerPluginFactories()scansauth-manager.tsfor itsawait import(...)sites and fails on any factory not accounted forbetter-auth-schema-parity.test.ts(#3624)organization,twoFactor,admin,phoneNumber,jwt,deviceAuthorizationauth-manager.ts#7820 gave the D7 gate its tripwire on the reasoning that "a plugin nobody loaded owns columns nobody compared". That reasoning applies verbatim to the parity gate, which never got one.
Why it is currently harmless, and why that is the fragile part
The six cover today's model-contributing plugins. Of the other eight the auth manager can assemble,
sso/scim/oauthProviderhave dedicated blocks or their own parity test, andbearer/haveIBeenPwned/magicLink/genericOAuth/customSessioncontribute no model surface at all right now.So the gap is entirely prospective, and it opens silently in two ways:
auth-manager.ts. D7 goes red until someone accounts for it; the parity gate says nothing and keeps passing.Either is the #3624 failure shape — an unprovisioned column better-auth writes, which surfaces as a runtime 500 (
team.memberCount) rather than as a red gate.Suggested direction (not a decision)
The cheapest fix is the one already proven in-repo: give the parity gate the same accounting-map-plus-scan shape as
AUTH_MANAGER_PLUGINS, where an entry either carries aconstructthunk (so declaring coverage and delivering it cannot come apart) or a writtenskipreason. Note the two gates deliberately callgetAuthTables()differently — parity passes theschema:options fromauth-schema-config.ts, D7 must not — so this is a shared reconciliation, not a shared plugin set. Worth checking whether the accounting map can be extracted and consumed by both without merging the two calls.Filed unassigned for triage. Backlinks: #7994 / PR #8121 (the D7 coverage expansion that surfaced it) - #7820 (where the D7 tripwire came from) - #3624 (the original parity hole).