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:
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.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.- 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
Observation
managed-extension-fields.test.ts(ADR-0105 D7) derives better-auth's owned column surface from a single plugin:auth-manager.tsassembles many more (beareralways;organization,twoFactor,admin,phoneNumber,jwt,deviceAuthorization,magicLink,genericOAuth,sso,scim, … behindAuthPluginConfigflags), and the sibling gatebetter-auth-schema-parity.test.tsalready 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_userdeclaresphone_numberas an ObjectStack extension field. better-auth'sphoneNumberplugin extends the user model and this repo maps it explicitly to that exact column —AUTH_PHONE_NUMBER_USER_FIELDSinauth-schema-config.ts:Measured against the pinned better-auth 1.7.0-rc.2, resolved
usercolumns:name, email, emailVerified, image, createdAt, updatedAttwoFactorEnabled, role, banned, banReason, banExpires, phone_number, phone_number_verifiedSo
sys_user.phone_numberis simultaneously a declared ObjectStack extension field and a column better-auth writes wheneverplugins.phoneNumberis 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_numberis the only overlap today. I checked the rest against the wider surface:manager_id,primary_business_unit_id,ai_access,sourceonsys_user, all ofsys_organizationand all ofsys_invitationare clean, and better-auth'srole/banned/ban_*/two_factor_enabledare 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:phone_numberis better-auth's, and itsMANAGED_EXTENSION_FIELDSentry is wrong. Drop it. Consequence: it stops being registered as an ObjectStack column, which is what the D2 write guard reads. It is not inMANAGED_EXTENSION_EDITABLE_FIELDS, so no generic write surface loses an affordance.phone_numberis ObjectStack's and the plugin must be mapped elsewhere. Costs a rename plus a migration, and contradicts the mappingauth-schema-config.tsalready ships.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_numberis 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_jwksare 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