Observation
managed-extension-fields.test.ts (the ADR-0105 D7 collision guard) derives better-auth's real field surface from getAuthTables() and fails on any overlap with MANAGED_EXTENSION_FIELDS. Its MODEL_TO_OBJECT map carries nine models — user, session, account, verification, organization, member, invitation, team, teamMember — and no apikey entry. Any object absent from that map is skipped outright:
constowned=byObject[object];if(!owned)continue;
sys_api_key declares managedBy: 'better-auth', so it looks like it is covered. It is not — its extension fields get zero collision coverage.
Why this is dormant today, not a live defect
better-auth's apiKey plugin is not loaded (auth-manager.ts assembles bearer + the optional set; apiKey is not among them). sys_api_key is hand-rolled: packages/core/src/security/api-key.ts mints and verifies it, and POST /api/v1/keys writes the row. So better-auth owns none of these columns, there is nothing to collide with, and the guard skipping the object is currently the correct answer.
Adding apikey: 'sys_api_key' to MODEL_TO_OBJECT today would change nothing either, since getAuthTables() is called without that plugin and would not emit an apikey model.
The latent part
If better-auth's apiKey plugin is ever enabled, its own model carries name, prefix, key, userId, enabled, expiresAt, permissions, metadata and the refill/rate-limit columns — several of which are exactly the columns sys_api_key already defines and writes. That is the ownership-transfer hazard D7 exists to catch, and the gate that is supposed to catch it would stay silent, because the object is not in its map.
Worth noting the shape: D7's guard is not wrong, it is unreachable for this object. That is the same class as objectstack#7727 (a declared row action behind a method gate that answered 405 first) — a check that reads as coverage and evaluates to nothing.
Suggested direction (not decided here)
Whoever enables better-auth's apiKey plugin must, in the same change, add apikey: 'sys_api_key' to MODEL_TO_OBJECT, pass the plugin into that test's getAuthTables() call, and reconcile the overlapping columns — most likely by keeping the hand-rolled table and NOT adopting the plugin's model, or by renaming ours. A cheaper interim option is a comment in MODEL_TO_OBJECT recording why apikey is deliberately absent, so its absence reads as a decision rather than an oversight.
Filing unassigned and finding-labeled: nothing a user hits today, and the trigger is a plugin-enablement decision nobody has taken.
Source
Found while implementing #7727 (PR #7769), which registers sys_api_key → {revoked} in MANAGED_EXTENSION_FIELDS / MANAGED_EXTENSION_EDITABLE_FIELDS. revoked is not among better-auth's apikey columns, so that entry is safe either way — but it is the first entry this object has ever had in that registry, which is what surfaced the gap.
Generated by Claude Code
Observation
managed-extension-fields.test.ts(the ADR-0105 D7 collision guard) derives better-auth's real field surface fromgetAuthTables()and fails on any overlap withMANAGED_EXTENSION_FIELDS. ItsMODEL_TO_OBJECTmap carries nine models —user,session,account,verification,organization,member,invitation,team,teamMember— and noapikeyentry. Any object absent from that map is skipped outright:sys_api_keydeclaresmanagedBy: 'better-auth', so it looks like it is covered. It is not — its extension fields get zero collision coverage.Why this is dormant today, not a live defect
better-auth's
apiKeyplugin is not loaded (auth-manager.tsassemblesbearer+ the optional set;apiKeyis not among them).sys_api_keyis hand-rolled:packages/core/src/security/api-key.tsmints and verifies it, andPOST /api/v1/keyswrites the row. So better-auth owns none of these columns, there is nothing to collide with, and the guard skipping the object is currently the correct answer.Adding
apikey: 'sys_api_key'toMODEL_TO_OBJECTtoday would change nothing either, sincegetAuthTables()is called without that plugin and would not emit anapikeymodel.The latent part
If better-auth's
apiKeyplugin is ever enabled, its own model carriesname,prefix,key,userId,enabled,expiresAt,permissions,metadataand the refill/rate-limit columns — several of which are exactly the columnssys_api_keyalready defines and writes. That is the ownership-transfer hazard D7 exists to catch, and the gate that is supposed to catch it would stay silent, because the object is not in its map.Worth noting the shape: D7's guard is not wrong, it is unreachable for this object. That is the same class as objectstack#7727 (a declared row action behind a method gate that answered 405 first) — a check that reads as coverage and evaluates to nothing.
Suggested direction (not decided here)
Whoever enables better-auth's
apiKeyplugin must, in the same change, addapikey: 'sys_api_key'toMODEL_TO_OBJECT, pass the plugin into that test'sgetAuthTables()call, and reconcile the overlapping columns — most likely by keeping the hand-rolled table and NOT adopting the plugin's model, or by renaming ours. A cheaper interim option is a comment inMODEL_TO_OBJECTrecording whyapikeyis deliberately absent, so its absence reads as a decision rather than an oversight.Filing unassigned and
finding-labeled: nothing a user hits today, and the trigger is a plugin-enablement decision nobody has taken.Source
Found while implementing #7727 (PR #7769), which registers
sys_api_key → {revoked}inMANAGED_EXTENSION_FIELDS/MANAGED_EXTENSION_EDITABLE_FIELDS.revokedis not among better-auth's apikey columns, so that entry is safe either way — but it is the first entry this object has ever had in that registry, which is what surfaced the gap.Generated by Claude Code