authPlugin: adoptBetterAuthTables() recipe + adoption docs (app User vs Auth identity)#509
Conversation
…ser vs Auth identity) Adds a convenience "adopt existing better-auth tables" recipe that presets the plugin-level schema + per-model modelName/fields knobs to match a pre-existing separate-schema better-auth install, so a migrator doesn't reconstruct the auth config from scratch. Combined with the keys/field derivation (#481) and schema placement (#482), the derived Auth lists diff clean (Schema parity) against the live database — no destructive auth migration. The app's own domain User is left untouched; linking it to the Auth identity is documented as the app's concern. - packages/auth/src/config/adopt-better-auth-tables.ts: pure recipe returning the AuthConfig adoption fragment (schema, modelNamePrefix, per-model field maps) - export adoptBetterAuthTables + its option/config types from the package root - tests: recipe defaults/options, composition with authPlugin, and a clean-diff adoption assertion (Auth lists land in the auth schema with @@map/@@Schema while the app's separate User stays in public, untouched) - docs: authentication guide section (app User vs Auth identity + linking + recipe), packages/auth CLAUDE.md + README, and migration-guide references - changeset: minor for @opensaas/stack-auth Implements #483 Part of #480 https://claude.ai/code/session_01ULd2HCT8dUve9aa9gKi6sX
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 80f0ebf The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
borisno2
left a comment
There was a problem hiding this comment.
Review — PR #509: adoptBetterAuthTables() recipe (D3 of #480, implements #483)
Verdict: APPROVE (posted as a Comment since the PR was authored by an integration identity). This is a clean, well-scoped, well-documented change. The recipe is a pure function that only sets adoption knobs, it spreads cleanly into authPlugin, and the clean-diff guarantee is genuinely asserted by the tests. One minor test-naming/coverage nit and a couple of doc/optional suggestions below — none blocking.
I reviewed the diff against the surrounding (merged) source: config/types.ts (AuthConfig/AuthModelConfig), config/index.ts (normalizeAuthConfig), config/derive-auth-lists.ts, and config/plugin.ts.
Correctness — verified against the real pipeline
- Returns a correct
AuthConfigfragment.AdoptBetterAuthTablesConfig = Pick<AuthConfig, 'schema' | 'user' | 'session' | 'account' | 'verification'>is concrete and spreads cleanly.buildModel()returnsAuthModelConfig({ modelName, fields? }); assigning it to thesessionslot (typedSessionConfig & AuthModelConfig) is sound because everySessionConfigmember is optional, so this composes without a cast. Confirmed. - Only adoption knobs, no side effects. The function is pure: it sets
schema+ per-modelmodelName(+ optionalfields) and nothing else. Providers,sessionFields,extendUserList, etc. all remain the developer's to add — exactly as documented. - Plugin-level
schemaactually reaches the generator.normalizeModelConfig(..., defaultSchema)(inconfig/index.ts) propagates the plugin-levelschemaonto everymodel.schema, andplugin.ts#beforeGeneratecollectsmodel.schema(not the plugin-level field) into the datasourceschemas. Soschema: 'auth'correctly lands on all four lists. Confirmed end-to-end. - Clean-diff / "app User untouched" guarantee holds and is asserted. Because the derived user key is
AuthUser(notUser),plugin.ts#init's add-vs-extend only extends a list that shares the derived key — the app'spublic.Useris never touched. The testlands every Auth list in the auth schema ... and leaves the app User untouchedasserts the real shapes:- each Auth list
dbis{ timestamps: true, map: 'Auth…', schema: 'auth' }(ADR-0004 auto-timestamps preserved +@@map+@@schema); - the app
UserkeepssubjectId, gains none ofemail/emailVerified/sessions, and stays inpublic; db.schemasis['public', 'auth'].
This is a genuine assertion of the adoption guarantee, not a rubber-stamp.
- each Auth list
- App-User ≠ Auth-identity separation is correct in code (separate keys) and clearly documented (auth guide section + linking subsection: linking is the app's concern via
relationship({ ref: 'AuthUser' })). - Field column maps flow through to field-level
@mapand the FK@map— asserted by thename: 'full_name'/session.userId: 'user_id'test againstfields.name.db?.mapandfields.user.db?.foreignKey. - No
any/ no casts in the new public surface.adopt-better-auth-tables.tsis fully typed; option and return types are concrete. (The pre-existingbetterAuthPlugins?: any[]inAuthConfigis out of scope here.) - Docs are accurate and non-destructive. The auth-guide section, README, CLAUDE.md, and both migration guides consistently frame this as "modelled for runtime/types, no destructive auth migration" with a Schema-parity diff check — no false claims of a migration.
Minor (non-blocking)
-
Test name vs. assertion mismatch —
'public'schema path is under-covered.adopt-better-auth-tables.test.ts:it('omits the schema when explicitly set to public', ...)
only assertsfragment.schemais'public'— it does not assert anything is "omitted." This matters because the behavior is the opposite of what the name implies: passingschema: 'public'setsmodel.schema = 'public'on all four models, sobeforeGenerateemitsschemas: ['public']and@@schema("public")on every Auth list (it is treated as an explicit single-schema install, not as "no schema"). That is arguably the intended behavior for "adopt a public-schema better-auth install," but the test name reads as a no-op-greenfield claim it doesn't verify. Suggest either (a) rename tokeeps the schema when explicitly set to public, or (b) add agenerationConfig()assertion showing the actual emitteddb.schemas/db.schemafor theschema: 'public'case so the behavior is pinned down. The recipe'sschemaJSDoc ("Pass'public'… for an install that is not on a separate schema") is consistent with (a)/the explicit-schema behavior. -
Greenfield/
schema: undefinedis not directly exercised here. Not required (the underlying schema-placement behavior is tested inplugin-schema-placement.test.ts, and the recipe always defaultsschema: 'auth'), so this is just a note, not a request. -
modelNamePrefix: ''escape hatch. The JSDoc rightly warns that an empty prefix yieldsUser/Session/... which would collide-and-extend an appUser. Good that it's documented; no code change needed. Optional: a one-line test assertingadoptBetterAuthTables({ modelNamePrefix: '' }).user.modelName === 'User'would lock the documented escape-hatch behavior.
Process
- Changeset present and correctly scoped (
@opensaas/stack-auth, minor) with a usage example. Noclaude-plugins/*changes, so no plugin-version bump needed — correct.
Nice work. The single thing worth tidying before merge is the test-name/coverage point (#1); everything else is optional polish.
Generated by Claude Code
Coverage Report for Core Package Coverage (./packages/core)
File CoverageNo changed files found. |
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Coverage Report for CLI Package Coverage (./packages/cli)
File CoverageNo changed files found. |
Coverage Report for Auth Package Coverage (./packages/auth)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Coverage Report for RAG Package Coverage (./packages/rag)
File CoverageNo changed files found. |
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
Implements #483
Part of #480
What this adds (D3 of PRD #480)
A convenience "adopt existing better-auth tables" recipe plus documentation that ties together the keys/field derivation (#481) and schema placement (#482) into a one-stop adoption path.
adoptBetterAuthTables()recipe (packages/auth/src/config/adopt-better-auth-tables.ts): a pure function returning theAuthConfigadoption fragment — the plugin-levelschemaplus each model'smodelName(and optional columnfieldsmaps) — preset to the conventions of a standard separate-schema better-auth install (AuthUser/AuthSession/AuthAccount/AuthVerificationin anauthschema). Spread it intoauthPluginalongside the rest of your config. Options:schema,modelNamePrefix, per-modelfields.adoptBetterAuthTables,AdoptBetterAuthTablesOptions,AdoptBetterAuthTablesConfig).User≠ Auth identity: because the recipe's derived user key isAuthUser(notUser), the plugin only ever adds/extends its derived keys — an app's own domainUseris never extended or overwritten. Linking the appUserto the Auth identity is documented as the application's concern (arelationship({ ref: 'AuthUser' })the app declares).Documentation
Uservs Auth identity, the recipe + customisation, Schema-parity / clean-diff check, and linking the two).packages/auth/CLAUDE.md+README.md: adoption-recipe subsection.migrating-from-keystone.md,migration.md): reference the adoption path (no destructive auth migration).Tests (
packages/auth/tests/adopt-better-auth-tables.test.ts)schema/modelNamePrefix/fieldsoptions.authPluginalongside the rest of the auth config.authschema with@@map+@@schema(auto-timestamps preserved) while the app's separatepublic.Userstays untouched; the datasource lists both schemas.@map/ FK@map.Verification
pnpm build— passpackages/authtests — 133 passed (8 files);packages/core— 562 passed;packages/cli— 266 passedpnpm lint— 0 errors (4 pre-existing warnings in untouched files)pnpm manypkg check— passpnpm format:check— passlink-check— passChangeset: minor for
@opensaas/stack-auth. Noclaude-plugins/*changes, so no plugin-version bump.https://claude.ai/code/session_01ULd2HCT8dUve9aa9gKi6sX
Generated by Claude Code