Skip to content

D7 collision guard derives a narrower plugin surface than the auth manager loads — sys_user.phone_number is a real overlap it cannot see #7820

Description

@os-help

Observation

managed-extension-fields.test.ts (ADR-0105 D7) derives better-auth's owned column surface from a single plugin:

getAuthTables({plugins: [organization({teams: {enabled: true}})]})

auth-manager.ts assembles many more (bearer always; organization, twoFactor, admin, phoneNumber, jwt, deviceAuthorization, magicLink, genericOAuth, sso, scim, … behind AuthPluginConfig flags), and the sibling gate better-auth-schema-parity.test.ts already derives its surface from that wider set with the reasoning written down: "Plugins that are feature-flagged off in some deployments are still included: the column has to exist before the flag can be turned on."

D7 makes the opposite choice and never says why. The consequence is measurable.

The overlap it cannot see

MANAGED_EXTENSION_FIELDS.sys_user declares phone_number as an ObjectStack extension field. better-auth's phoneNumber plugin extends the user model and this repo maps it explicitly to that exact column — AUTH_PHONE_NUMBER_USER_FIELDS in auth-schema-config.ts:

phoneNumber -> phone_number
phoneNumberVerified -> phone_number_verified

Measured against the pinned better-auth 1.7.0-rc.2, resolved user columns:

  • with D7's plugin set: name, email, emailVerified, image, createdAt, updatedAt
  • with the auth manager's set: the same plus twoFactorEnabled, role, banned, banReason, banExpires, phone_number, phone_number_verified

So sys_user.phone_number is simultaneously a declared ObjectStack extension field and a column better-auth writes whenever plugins.phoneNumber is on. That is the ownership-transfer condition D7 exists to fail on, and D7 reports green because the plugin that owns the column is not in its derivation.

phone_number is the only overlap today. I checked the rest against the wider surface: manager_id, primary_business_unit_id, ai_access, source on sys_user, all of sys_organization and all of sys_invitation are clean, and better-auth's role/banned/ban_*/two_factor_enabled are not claimed by the registry.

What the decision actually is

Not "widen the plugin list and go green" — widening it turns D7 red on sys_user.phone_number, which is the honest outcome and a real question:

  1. phone_number is better-auth's, and its MANAGED_EXTENSION_FIELDS entry is wrong. Drop it. Consequence: it stops being registered as an ObjectStack column, which is what the D2 write guard reads. It is not in MANAGED_EXTENSION_EDITABLE_FIELDS, so no generic write surface loses an affordance.
  2. phone_number is ObjectStack's and the plugin must be mapped elsewhere. Costs a rename plus a migration, and contradicts the mapping auth-schema-config.ts already ships.
  3. Both, deliberately shared — which D7's own header calls the thing that must never happen ("one side clobbers the other with no error").

Reading 1 looks right on the evidence, but it is an ownership call on an identity column, so filing rather than guessing.

Severity note

Bounded today: phone_number is declared but not generically editable, so no generic write path targets it, and the plugin is opt-in. What is not bounded is the gate — D7 will keep answering green about a column it is not looking at, and the same blindness covers every other plugin outside its list.

Related

Found while implementing #7770 (the unmapped-object half of the same guard). #7770's fix accounts for objects the map skips; this is the other axis — columns the derivation skips because the owning plugin is not loaded. sys_two_factor / sys_device_code / sys_jwks are registered in that PR's new exemption registry with exactly this reason, and each carries the note that the exemption holds only while no extension field is declared on them.

Nothing in #7770's PR changes the plugin set, deliberately: doing so is this issue.

Filing unassigned.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions