@opensaas/stack-auth 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +109 -0
  3. package/CLAUDE.md +104 -7
  4. package/README.md +18 -7
  5. package/dist/config/adopt-better-auth-tables.d.ts +47 -0
  6. package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
  7. package/dist/config/adopt-better-auth-tables.js +29 -1
  8. package/dist/config/adopt-better-auth-tables.js.map +1 -1
  9. package/dist/config/derive-auth-lists.d.ts +7 -5
  10. package/dist/config/derive-auth-lists.d.ts.map +1 -1
  11. package/dist/config/derive-auth-lists.js +33 -34
  12. package/dist/config/derive-auth-lists.js.map +1 -1
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/index.js +41 -11
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/plugin.d.ts.map +1 -1
  17. package/dist/config/plugin.js +39 -27
  18. package/dist/config/plugin.js.map +1 -1
  19. package/dist/config/types.d.ts +126 -24
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/server/index.d.ts +29 -3
  22. package/dist/server/index.d.ts.map +1 -1
  23. package/dist/server/index.js +193 -66
  24. package/dist/server/index.js.map +1 -1
  25. package/package.json +3 -3
  26. package/src/config/adopt-better-auth-tables.ts +70 -1
  27. package/src/config/derive-auth-lists.ts +37 -38
  28. package/src/config/index.ts +47 -12
  29. package/src/config/plugin.ts +39 -27
  30. package/src/config/types.ts +127 -21
  31. package/src/server/index.ts +244 -80
  32. package/tests/adopt-better-auth-tables.test.ts +99 -0
  33. package/tests/config.test.ts +66 -8
  34. package/tests/derive-auth-lists.test.ts +79 -5
  35. package/tests/generated-fk-shape.test.ts +65 -0
  36. package/tests/plugin-derived-keys.test.ts +48 -0
  37. package/tests/server.test.ts +517 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-auth@0.36.0 build /home/runner/work/stack/stack/packages/auth
2
+ > @opensaas/stack-auth@0.37.0 build /home/runner/work/stack/stack/packages/auth
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # @opensaas/stack-auth
2
2
 
3
+ ## 0.37.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#872](https://github.com/OpenSaasAU/stack/pull/872) [`17acf04`](https://github.com/OpenSaasAU/stack/commit/17acf046b494da184c2b77434a7b4d3400ca32f2) Thanks [@borisno2](https://github.com/borisno2)! - `createAuth()` now forwards `AuthConfig` options it previously normalized but silently dropped: `emailAndPassword.minPasswordLength`, `passwordReset.enabled`/`tokenExpiration` (wired to better-auth's `sendResetPassword`), and `emailVerification.enabled`/`sendOnSignUp`/`tokenExpiration` (wired to `sendVerificationEmail`).
8
+
9
+ The stack does not wrap these email callbacks in any way — `emailAndPassword.sendResetPassword` and `emailVerification.sendVerificationEmail` are better-auth's own option shape, forwarded straight through, so an app configures them exactly as it would when calling `betterAuth()` directly:
10
+
11
+ ```typescript
12
+ authPlugin({
13
+ emailAndPassword: {
14
+ enabled: true,
15
+ sendResetPassword: async ({ user, url }) => {
16
+ await resend.emails.send({
17
+ to: user.email,
18
+ subject: 'Reset your password',
19
+ html: `<a href="${url}">Reset your password</a>`,
20
+ })
21
+ },
22
+ },
23
+ emailVerification: {
24
+ enabled: true,
25
+ sendVerificationEmail: async ({ user, url }) => {
26
+ await resend.emails.send({
27
+ to: user.email,
28
+ subject: 'Verify your email',
29
+ html: `<a href="${url}">Verify your email</a>`,
30
+ })
31
+ },
32
+ },
33
+ })
34
+ ```
35
+
36
+ If not provided, reset/verification emails are logged to console instead of sent — apps relying on the previous no-op behavior (verification/reset emails silently not sending) will start sending real emails once `emailVerification`/`passwordReset` are enabled and these callbacks are configured.
37
+
38
+ Two related fixes, both changing existing behavior:
39
+
40
+ - `session.updateAge` is retyped from `boolean` to `number | false` — the number of seconds between session refreshes, passed straight through to better-auth's own `session.updateAge` instead of being computed as `expiresIn / 10`. The default changes from `true` to `86400` (1 day), matching better-auth's own default. Update any `updateAge: true` config to a duration in seconds (e.g. `86400`). `updateAge: false` now correctly maps to better-auth's `disableSessionRefresh: true` (previously it mapped to `updateAge: 0`, which better-auth treats as "refresh on every request" — the opposite of disabling refresh).
41
+ - `getSessionFromAuth(auth, sessionFields, headers)` gains a required third `headers: Headers` parameter. Previously it always called `auth.api.getSession({ headers: new Headers() })`, an empty header set that could never resolve a session cookie, so the function always returned `null`. Callers must now pass the request's real headers (e.g. Next.js `await headers()`).
42
+
43
+ Setting `emailAndPassword.requireConfirmation` (while `emailAndPassword.enabled` is true) now logs a `console.warn` — it has no better-auth server-side equivalent (it's a UI-only "confirm password" concern). Pass `requirePasswordConfirmation` directly to `<SignUpForm>`/`<ResetPasswordForm>` instead. Similarly, `passwordReset.enabled` now warns if `emailAndPassword.enabled` is false, since password reset has no effect without a password-based account.
44
+
45
+ - [#871](https://github.com/OpenSaasAU/stack/pull/871) [`06375ca`](https://github.com/OpenSaasAU/stack/commit/06375cad571677e92bfe84c35ff55240f3546a1f) Thanks [@{](https://github.com/{)! - Add a per-model `tableName` option, independent of `modelName`, so a renamed Auth list key can still adopt a differently-named live table — most commonly better-auth's own default lowercase table names (`user`, `session`, `account`, `verification`).
46
+
47
+ ```typescript
48
+ authPlugin({
49
+ modelName: 'AuthUser', tableName: 'user' },
50
+ session: { modelName: 'AuthSession', tableName: 'session' },
51
+ })
52
+ ```
53
+
54
+ `adoptBetterAuthTables()` gains matching `useBetterAuthTableNames` and `tableNames` options:
55
+
56
+ ```typescript
57
+ adoptBetterAuthTables({ useBetterAuthTableNames: true })
58
+ // or explicitly:
59
+ adoptBetterAuthTables({ tableNames: { user: 'user', session: 'session' } })
60
+ ```
61
+
62
+ With no `tableName` set, behaviour is unchanged: the table name still follows `modelName` when it differs from the better-auth default, otherwise no `@@map` is emitted.
63
+
64
+ - [#874](https://github.com/OpenSaasAU/stack/pull/874) [`7ef9dbc`](https://github.com/OpenSaasAU/stack/commit/7ef9dbc2f94cc4e7ab831ecafb3ef65159a3c55e) Thanks [@borisno2](https://github.com/borisno2)! - Add a `betterAuthOptions` escape hatch on `AuthConfig` for better-auth options the stack doesn't model, plus an exported `buildBetterAuthOptions()` builder for apps that still need to hand-wire their own `betterAuth()` instance.
65
+
66
+ `betterAuthOptions` is deep-merged onto the options `createAuth()` builds, applied last — a plain-object value merges recursively alongside sibling keys the stack already set (e.g. `session: { cookieCache }` doesn't clobber `session.expiresIn`), and wins on any genuine key collision:
67
+
68
+ ```typescript
69
+ authPlugin({
70
+ betterAuthOptions: {
71
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
72
+ session: { cookieCache: { enabled: true, maxAge: 300 } },
73
+ verification: { storeIdentifier: 'hashed' },
74
+ baseURL: process.env.BETTER_AUTH_URL,
75
+ },
76
+ })
77
+ ```
78
+
79
+ `database`, `plugins`, and `additionalFields` under `user`/`session`/`account`/`verification` are rejected — they already have dedicated seams (`db` config, `betterAuthPlugins`), or have schema consequences a passthrough can't also apply to the generated Prisma schema.
80
+
81
+ `buildBetterAuthOptions(config, context)` returns the exact same options object `createAuth()` uses, for apps that need a resolved `betterAuth()` instance rather than `createAuth()`'s lazy proxy:
82
+
83
+ ```typescript
84
+ import { betterAuth } from 'better-auth'
85
+ import { buildBetterAuthOptions } from '@opensaas/stack-auth/server'
86
+
87
+ export const auth = betterAuth({
88
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext)),
89
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
90
+ })
91
+ ```
92
+
93
+ - [#870](https://github.com/OpenSaasAU/stack/pull/870) [`7b6189f`](https://github.com/OpenSaasAU/stack/commit/7b6189fa60119a45082ba62dd71d915d93de529c) Thanks [@relationship({](https://github.com/relationship({)! - A relationship field's foreign key can now be declared non-nullable via `db.isNullable: false` — the generated FK column and its relation field lose their `?` together. Omitting the option leaves every existing relationship unchanged (still nullable by default).
94
+
95
+ ```typescript
96
+
97
+ ref: 'User.sessions',
98
+ db: { isNullable: false },
99
+ })
100
+ // Generates: userId String (was String?)
101
+ // user User @relation(...) (was User?)
102
+ ```
103
+
104
+ `@opensaas/stack-auth`'s derived Auth lists now use this to match better-auth's own Prisma schema: `Session.expiresAt`, `Verification.expiresAt`, and the `Session.user`/`Account.user` foreign keys generate as required instead of nullable.
105
+
106
+ **Migration note:** this changes the generated schema for existing greenfield apps. Running `opensaas generate` followed by `prisma db push`/`prisma migrate dev` will produce a migration that adds `NOT NULL` to `Session.expiresAt`, `Verification.expiresAt`, `Session.userId`, and `Account.userId`. Since better-auth's own adapter always writes these columns, no existing row should violate the new constraint — but back up production data before applying, as with any schema migration.
107
+
108
+ ### Patch Changes
109
+
110
+ - [#867](https://github.com/OpenSaasAU/stack/pull/867) [`43b4d17`](https://github.com/OpenSaasAU/stack/commit/43b4d1738340f05b1cf8bec3315927b3004816dd) Thanks [@borisno2](https://github.com/borisno2)! - Fix a better-auth plugin's schema extension of a base model (`user`/`session`/`account`/`verification`) silently dropping the derived Auth list's `db` (`map`/`schema`/`timestamps`) and `access` config.
111
+
3
112
  ## 0.36.0
4
113
 
5
114
  ## 0.35.0
package/CLAUDE.md CHANGED
@@ -29,6 +29,7 @@ Auto-generated lists:
29
29
  ### Server (`src/server/index.ts`)
30
30
 
31
31
  - `createAuth(config, rawContext?)` - Creates Better-auth instance with MCP plugin support
32
+ - `buildBetterAuthOptions(config, rawContext?)` - Returns the same `BetterAuthOptions` `createAuth()` builds, without constructing an instance — for apps that need to hand-wire their own `betterAuth()`
32
33
  - Returns `{ handler, signIn, signOut, ... }` - Better-auth methods
33
34
 
34
35
  ### Client (`src/client/index.ts`)
@@ -71,13 +72,16 @@ developer writes — not hardcoded. The pure derivation lives in
71
72
  `src/config/derive-auth-lists.ts` (`deriveAuthLists`), which `getAuthLists`
72
73
  and the plugin's add-vs-extend logic consume:
73
74
 
74
- - per-model `modelName` → list key + table `@@map`
75
+ - per-model `modelName` → list key (and Prisma model name)
76
+ - per-model `tableName` → table `@@map`, **independent of `modelName`**
77
+ (defaults to `modelName` when it differs from the better-auth default,
78
+ otherwise unset — i.e. unchanged output when `tableName` isn't set)
75
79
  - per-model `fields` (better-auth field → column) → field-level `@map`
76
80
  - the `userId` column override → the `user` relationship foreign-key `@map`
77
81
  - relationship refs between the Auth lists follow the derived keys
78
82
  (e.g. `Session.user → AuthUser.sessions`)
79
83
 
80
- With no `modelName`/`fields` overrides the output is unchanged
84
+ With no `modelName`/`tableName`/`fields` overrides the output is unchanged
81
85
  (`User`/`Session`/`Account`/`Verification`, original field shapes, no `@@map`).
82
86
 
83
87
  ```typescript
@@ -89,6 +93,15 @@ authPlugin({
89
93
  // Adds AuthUser/AuthSession/... and leaves an app's own `User` untouched.
90
94
  ```
91
95
 
96
+ ```typescript
97
+ // modelName sets the list key; tableName independently pins the live table —
98
+ // e.g. a prefixed list key adopting better-auth's own default lowercase table.
99
+ authPlugin({
100
+ user: { modelName: 'AuthUser', tableName: 'user' },
101
+ session: { modelName: 'AuthSession', tableName: 'session' },
102
+ })
103
+ ```
104
+
92
105
  Because the plugin only ever adds/extends its **derived** keys, an app's own
93
106
  domain `User` (a different model from the better-auth user) is never extended
94
107
  or overwritten when the user model is renamed. The runtime `getUser`/
@@ -164,10 +177,10 @@ How it wires up (Postgres multi-schema):
164
177
 
165
178
  `adoptBetterAuthTables()` (`src/config/adopt-better-auth-tables.ts`) is a thin
166
179
  recipe that returns the `AuthConfig` adoption knobs — the plugin-level `schema`
167
- plus a per-model `modelName` (and optional column `fields` maps) — preset to the
168
- conventions of a standard separate-schema better-auth install. It ties together
169
- the keys/field derivation and schema placement so a migrator doesn't rebuild the
170
- config by hand. Spread it into `authPlugin`:
180
+ plus a per-model `modelName` (and optional column `fields`/`tableName` maps) —
181
+ preset to the conventions of a standard separate-schema better-auth install. It
182
+ ties together the keys/field derivation and schema placement so a migrator
183
+ doesn't rebuild the config by hand. Spread it into `authPlugin`:
171
184
 
172
185
  ```typescript
173
186
  import { authPlugin, adoptBetterAuthTables } from '@opensaas/stack-auth'
@@ -176,7 +189,23 @@ authPlugin({
176
189
  ...adoptBetterAuthTables(), // schema: 'auth', AuthUser/AuthSession/AuthAccount/AuthVerification
177
190
  emailAndPassword: { enabled: true },
178
191
  })
179
- // Options: adoptBetterAuthTables({ schema, modelNamePrefix, fields })
192
+ // Options: adoptBetterAuthTables({ schema, modelNamePrefix, fields, useBetterAuthTableNames, tableNames })
193
+ ```
194
+
195
+ The most common adoption shape is a project that ran better-auth **before**
196
+ Stack, so its live tables are still better-auth's own default lowercase names
197
+ (`user`/`session`/`account`/`verification`) even though the derived list keys
198
+ need an `Auth` prefix to avoid colliding with the app's own domain `User`.
199
+ `useBetterAuthTableNames: true` sets every model's `tableName` to that
200
+ default; the per-model `tableNames` map is the escape hatch for a mix (it
201
+ wins over `useBetterAuthTableNames` for any model it names):
202
+
203
+ ```typescript
204
+ authPlugin({
205
+ ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
206
+ // → AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
207
+ // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
208
+ })
180
209
  ```
181
210
 
182
211
  It is pure config (no side effects): everything it sets can also be written
@@ -294,6 +323,74 @@ authPlugin({
294
323
  })
295
324
  ```
296
325
 
326
+ ### Escape hatch for unmodelled better-auth options (`betterAuthOptions`)
327
+
328
+ `AuthConfig` models a deliberately closed set of better-auth options. For
329
+ anything the stack doesn't model — database hooks, `session.cookieCache`,
330
+ `baseURL`, `verification.storeIdentifier`, and so on — pass it through
331
+ `betterAuthOptions`, typed as better-auth's own `BetterAuthOptions` so it
332
+ tracks better-auth's surface without the stack re-declaring it:
333
+
334
+ ```typescript
335
+ authPlugin({
336
+ betterAuthOptions: {
337
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
338
+ session: { cookieCache: { enabled: true, maxAge: 300 } },
339
+ verification: { storeIdentifier: 'hashed' },
340
+ baseURL: process.env.BETTER_AUTH_URL,
341
+ },
342
+ })
343
+ ```
344
+
345
+ `createAuth()` (`src/server/index.ts`) deep-merges `betterAuthOptions` onto
346
+ the options it builds from the rest of `AuthConfig`, applied **last**:
347
+ plain-object values merge recursively per key (so `session: { cookieCache }`
348
+ lands alongside the stack's own `session.expiresIn`/`updateAge` instead of
349
+ replacing the whole `session` block), arrays and other value types replace
350
+ outright, and `betterAuthOptions` wins on any genuine key collision. The
351
+ merge and the option-building it merges onto both live in
352
+ `buildBetterAuthOptions()`, the single place `createAuth()` and the exported
353
+ builder share — they cannot drift from each other because `createAuth()`
354
+ calls it directly rather than reimplementing it.
355
+
356
+ `database` and `plugins` are rejected outright (`assertNoUnsupportedPassthroughKeys`
357
+ in `src/server/index.ts`): they already have dedicated seams (`db` in the
358
+ stack config, and `betterAuthPlugins` respectively), and accepting them here
359
+ would create two unranked ways to set the same thing — worse for `plugins`,
360
+ since the stack must append `nextCookies()` last (see "Auth forms submit
361
+ through server actions" below). `additionalFields` under `user`/`session`/
362
+ `account`/`verification` is rejected too — it adds columns with no
363
+ corresponding change to the generated Prisma schema, which is exactly the
364
+ silent-divergence failure mode this passthrough exists to avoid elsewhere.
365
+ Add fields to the derived list instead: `extendUserList` for the user model,
366
+ or declare the list yourself in your own `lists` config for the others (the
367
+ auth plugin's `addList`-vs-`extendList` logic — see "Deriving Auth lists from
368
+ better-auth config" above — merges in field additions for any list matching
369
+ one of its derived keys).
370
+
371
+ ### Hand-wiring `betterAuth()` from the stack config (`buildBetterAuthOptions`)
372
+
373
+ An app that needs a resolved `betterAuth()` instance at module-init time
374
+ (rather than `createAuth()`'s lazy proxy) can derive its options from the
375
+ stack config instead of duplicating them:
376
+
377
+ ```typescript
378
+ import { betterAuth } from 'better-auth'
379
+ import { buildBetterAuthOptions } from '@opensaas/stack-auth/server'
380
+
381
+ export const auth = betterAuth({
382
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext)),
383
+ databaseHooks: { user: { create: { after: syncDomainUser } } }, // not yet in betterAuthOptions
384
+ })
385
+ ```
386
+
387
+ This is the same async-resolve-then-construct step `createAuth()` performs
388
+ internally, exported standalone — see ADR-0014 and root `CLAUDE.md`'s
389
+ "Getting the ORM client outside a request" for why `createAuth()` itself
390
+ can't be synchronous. It gives an incremental path onto `createAuth()`: adopt
391
+ the builder first, then fold options into `betterAuthOptions` above as the
392
+ stack grows first-class config for them.
393
+
297
394
  ## Integration Points
298
395
 
299
396
  ### With @opensaas/stack-core
package/README.md CHANGED
@@ -164,7 +164,15 @@ authPlugin({
164
164
  emailAndPassword: {
165
165
  enabled: true,
166
166
  minPasswordLength: 8,
167
- requireConfirmation: true,
167
+ requireConfirmation: true, // UI-only — pass to <SignUpForm>/<ResetPasswordForm> directly
168
+ // Passed straight through to better-auth's own `emailAndPassword.sendResetPassword`
169
+ sendResetPassword: async ({ user, url }) => {
170
+ await yourEmailService.send({
171
+ to: user.email,
172
+ subject: 'Reset your password',
173
+ html: `<a href="${url}">Reset your password</a>`,
174
+ })
175
+ },
168
176
  },
169
177
 
170
178
  // Email verification
@@ -172,6 +180,14 @@ authPlugin({
172
180
  enabled: true,
173
181
  sendOnSignUp: true,
174
182
  tokenExpiration: 86400, // 24 hours in seconds
183
+ // Passed straight through to better-auth's own `emailVerification.sendVerificationEmail`
184
+ sendVerificationEmail: async ({ user, url }) => {
185
+ await yourEmailService.send({
186
+ to: user.email,
187
+ subject: 'Verify your email',
188
+ html: `<a href="${url}">Verify your email</a>`,
189
+ })
190
+ },
175
191
  },
176
192
 
177
193
  // Password reset
@@ -195,7 +211,7 @@ authPlugin({
195
211
  // Session configuration
196
212
  session: {
197
213
  expiresIn: 604800, // 7 days in seconds
198
- updateAge: true, // Refresh session on each request
214
+ updateAge: 86400, // seconds between session refreshes; set `false` to disable
199
215
  },
200
216
 
201
217
  // Fields to include in session object
@@ -208,11 +224,6 @@ authPlugin({
208
224
  company: text(),
209
225
  },
210
226
  },
211
-
212
- // Custom email sending function
213
- sendEmail: async ({ to, subject, html }) => {
214
- await yourEmailService.send({ to, subject, html })
215
- },
216
227
  })
217
228
  ```
218
229
 
@@ -28,6 +28,22 @@
28
28
  * auth migration. The recipe never touches the application's own domain `User`:
29
29
  * its model names are `Auth`-prefixed by default and the plugin only ever
30
30
  * adds/extends its *derived* keys.
31
+ *
32
+ * The single most common adoption shape is a project that ran better-auth
33
+ * *before* adding Stack: its live tables are still better-auth's own default
34
+ * lowercase names (`user`/`session`/`account`/`verification`), even though the
35
+ * derived list keys need the `Auth` prefix to avoid colliding with the app's
36
+ * own domain `User`. Pass `useBetterAuthTableNames: true` to point every
37
+ * model's physical table at that default while keeping the prefixed list keys
38
+ * (or `tableNames` for an explicit per-model override):
39
+ *
40
+ * ```typescript
41
+ * authPlugin({
42
+ * ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
43
+ * // AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
44
+ * // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
45
+ * })
46
+ * ```
31
47
  */
32
48
  import type { AuthConfig } from './types.js';
33
49
  /**
@@ -85,6 +101,37 @@ export type AdoptBetterAuthTablesOptions = {
85
101
  account?: Record<string, string>;
86
102
  verification?: Record<string, string>;
87
103
  };
104
+ /**
105
+ * Set every model's physical table name to better-auth's own default
106
+ * lowercase table name (`user`, `session`, `account`, `verification`) —
107
+ * independent of the prefixed list key/`modelName`.
108
+ *
109
+ * This is the single most common adoption shape: a project that ran
110
+ * better-auth before adding the stack has exactly these tables, and the
111
+ * default `modelNamePrefix: 'Auth'` alone would otherwise pin the table
112
+ * name to the prefixed model name (`AuthUser`, ...), which `prisma migrate
113
+ * diff` reads as a rename against the live `user` table.
114
+ *
115
+ * Ignored for a model with an explicit entry in {@link tableNames}.
116
+ *
117
+ * @default false
118
+ */
119
+ useBetterAuthTableNames?: boolean;
120
+ /**
121
+ * Per-model explicit table name overrides, keyed by model. Takes
122
+ * precedence over `useBetterAuthTableNames` for that model.
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * adoptBetterAuthTables({ tableNames: { user: 'users' } })
127
+ * ```
128
+ */
129
+ tableNames?: {
130
+ user?: string;
131
+ session?: string;
132
+ account?: string;
133
+ verification?: string;
134
+ };
88
135
  };
89
136
  /**
90
137
  * The adoption-relevant slice of {@link AuthConfig}: the plugin-level `schema`
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACtC,CAAA;CACF,CAAA;AAUD;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,UAAU,EACV,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAC3D,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAqB7B"}
1
+ {"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACtC,CAAA;IAED;;;;;;;;;;;;;;OAcG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;IAEjC;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF,CAAA;AAkBD;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,UAAU,EACV,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAC3D,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAiC7B"}
@@ -28,6 +28,22 @@
28
28
  * auth migration. The recipe never touches the application's own domain `User`:
29
29
  * its model names are `Auth`-prefixed by default and the plugin only ever
30
30
  * adds/extends its *derived* keys.
31
+ *
32
+ * The single most common adoption shape is a project that ran better-auth
33
+ * *before* adding Stack: its live tables are still better-auth's own default
34
+ * lowercase names (`user`/`session`/`account`/`verification`), even though the
35
+ * derived list keys need the `Auth` prefix to avoid colliding with the app's
36
+ * own domain `User`. Pass `useBetterAuthTableNames: true` to point every
37
+ * model's physical table at that default while keeping the prefixed list keys
38
+ * (or `tableNames` for an explicit per-model override):
39
+ *
40
+ * ```typescript
41
+ * authPlugin({
42
+ * ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
43
+ * // AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
44
+ * // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
45
+ * })
46
+ * ```
31
47
  */
32
48
  /** The four better-auth models and their default (unprefixed) model names. */
33
49
  const MODEL_DEFAULT_NAMES = {
@@ -36,6 +52,13 @@ const MODEL_DEFAULT_NAMES = {
36
52
  account: 'Account',
37
53
  verification: 'Verification',
38
54
  };
55
+ /** better-auth's own default lowercase table names, per model. */
56
+ const BETTER_AUTH_DEFAULT_TABLE_NAMES = {
57
+ user: 'user',
58
+ session: 'session',
59
+ account: 'account',
60
+ verification: 'verification',
61
+ };
39
62
  /**
40
63
  * Build the adoption {@link AuthConfig} fragment for a pre-existing better-auth
41
64
  * installation.
@@ -48,11 +71,16 @@ const MODEL_DEFAULT_NAMES = {
48
71
  * (and any field column maps) set to match the live tables
49
72
  */
50
73
  export function adoptBetterAuthTables(options = {}) {
51
- const { schema = 'auth', modelNamePrefix = 'Auth', fields = {} } = options;
74
+ const { schema = 'auth', modelNamePrefix = 'Auth', fields = {}, useBetterAuthTableNames = false, tableNames = {}, } = options;
52
75
  const buildModel = (model) => {
53
76
  const config = {
54
77
  modelName: `${modelNamePrefix}${MODEL_DEFAULT_NAMES[model]}`,
55
78
  };
79
+ const tableName = tableNames[model] ??
80
+ (useBetterAuthTableNames ? BETTER_AUTH_DEFAULT_TABLE_NAMES[model] : undefined);
81
+ if (tableName !== undefined) {
82
+ config.tableName = tableName;
83
+ }
56
84
  const fieldMap = fields[model];
57
85
  if (fieldMap && Object.keys(fieldMap).length > 0) {
58
86
  config.fields = fieldMap;
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA+DH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAYV;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAO,GAAiC,EAAE;IAE1C,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,eAAe,GAAG,MAAM,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;IAE1E,MAAM,UAAU,GAAG,CAAC,KAAuC,EAAmB,EAAE;QAC9E,MAAM,MAAM,GAAoB;YAC9B,SAAS,EAAE,GAAG,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE;SAC7D,CAAA;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,IAAI,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAA;QAC1B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC;QACxB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC;KACzC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAgGH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAEV,kEAAkE;AAClE,MAAM,+BAA+B,GAAG;IACtC,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAYV;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAO,GAAiC,EAAE;IAE1C,MAAM,EACJ,MAAM,GAAG,MAAM,EACf,eAAe,GAAG,MAAM,EACxB,MAAM,GAAG,EAAE,EACX,uBAAuB,GAAG,KAAK,EAC/B,UAAU,GAAG,EAAE,GAChB,GAAG,OAAO,CAAA;IAEX,MAAM,UAAU,GAAG,CAAC,KAAuC,EAAmB,EAAE;QAC9E,MAAM,MAAM,GAAoB;YAC9B,SAAS,EAAE,GAAG,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE;SAC7D,CAAA;QACD,MAAM,SAAS,GACb,UAAU,CAAC,KAAK,CAAC;YACjB,CAAC,uBAAuB,CAAC,CAAC,CAAC,+BAA+B,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,CAAC,SAAS,GAAG,SAAS,CAAA;QAC9B,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,IAAI,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAA;QAC1B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC;QACxB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC;KACzC,CAAA;AACH,CAAC"}
@@ -2,13 +2,15 @@
2
2
  * Pure `better-auth config → Auth lists` derivation.
3
3
  *
4
4
  * This module is intentionally free of side effects and plugin/runtime
5
- * concerns: given the resolved better-auth model config (per-model `modelName`
6
- * and `fields` column maps) plus any custom User fields, it produces the four
7
- * OpenSaaS Auth lists (user/session/account/verification) with:
5
+ * concerns: given the resolved better-auth model config (per-model `modelName`,
6
+ * `tableName`, and `fields` column maps) plus any custom User fields, it
7
+ * produces the four OpenSaaS Auth lists (user/session/account/verification)
8
+ * with:
8
9
  *
9
10
  * - list keys taken from each model's `modelName`
10
- * - a table `@@map` (list-level `db.map`) when the key differs from the
11
- * default better-auth model name
11
+ * - a table `@@map` (list-level `db.map`) taken from each model's resolved
12
+ * `tableName` independent of `modelName`, so a renamed list key can still
13
+ * adopt a differently-named live table
12
14
  * - field-level `@map` (`db.map`) for any better-auth field → column override
13
15
  * - relationship refs between the auth lists wired to the *derived* keys
14
16
  * (e.g. `Session.user → AuthUser.sessions`)
@@ -1 +1 @@
1
- {"version":3,"file":"derive-auth-lists.d.ts","sourceRoot":"","sources":["../../src/config/derive-auth-lists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAA6B,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAanG;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oDAAoD;IACpD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,kEAAkE;IAElE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CACvC,CAAA;AAyMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,UAAU,GAAE,oBAAyB,EACrC,YAAY,GAAE,gBAAqB,GAClC,gBAAgB,CAiBlB"}
1
+ {"version":3,"file":"derive-auth-lists.d.ts","sourceRoot":"","sources":["../../src/config/derive-auth-lists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAA6B,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEnG;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oDAAoD;IACpD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,kEAAkE;IAElE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CACvC,CAAA;AAiND;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,UAAU,GAAE,oBAAyB,EACrC,YAAY,GAAE,gBAAqB,GAClC,gBAAgB,CAiBlB"}
@@ -2,13 +2,15 @@
2
2
  * Pure `better-auth config → Auth lists` derivation.
3
3
  *
4
4
  * This module is intentionally free of side effects and plugin/runtime
5
- * concerns: given the resolved better-auth model config (per-model `modelName`
6
- * and `fields` column maps) plus any custom User fields, it produces the four
7
- * OpenSaaS Auth lists (user/session/account/verification) with:
5
+ * concerns: given the resolved better-auth model config (per-model `modelName`,
6
+ * `tableName`, and `fields` column maps) plus any custom User fields, it
7
+ * produces the four OpenSaaS Auth lists (user/session/account/verification)
8
+ * with:
8
9
  *
9
10
  * - list keys taken from each model's `modelName`
10
- * - a table `@@map` (list-level `db.map`) when the key differs from the
11
- * default better-auth model name
11
+ * - a table `@@map` (list-level `db.map`) taken from each model's resolved
12
+ * `tableName` independent of `modelName`, so a renamed list key can still
13
+ * adopt a differently-named live table
12
14
  * - field-level `@map` (`db.map`) for any better-auth field → column override
13
15
  * - relationship refs between the auth lists wired to the *derived* keys
14
16
  * (e.g. `Session.user → AuthUser.sessions`)
@@ -25,16 +27,6 @@
25
27
  */
26
28
  import { list } from '@opensaas/stack-core';
27
29
  import { text, timestamp, checkbox, relationship } from '@opensaas/stack-core/fields';
28
- /**
29
- * Default better-auth model names — used to decide whether a `@@map` is needed
30
- * (only when the configured `modelName` differs from the default).
31
- */
32
- const DEFAULT_MODEL_NAMES = {
33
- user: 'User',
34
- session: 'Session',
35
- account: 'Account',
36
- verification: 'Verification',
37
- };
38
30
  /**
39
31
  * Build the list-level `db` config (`timestamps` + `@@map` + `@@schema`) for a
40
32
  * derived list.
@@ -46,21 +38,20 @@ const DEFAULT_MODEL_NAMES = {
46
38
  * Auth list must opt back in so the generated models keep those columns and
47
39
  * better-auth keeps working.
48
40
  *
49
- * When the developer renames the model (e.g. `modelName: 'AuthUser'`), we also
50
- * pin the physical table name to that model name via `@@map("AuthUser")` so the
51
- * generated list adopts the developer's live table exactly. When a `schema` is
52
- * configured (plugin-level or per-model), the list is placed in that Postgres
53
- * schema via `@@schema(...)`.
41
+ * The physical table name (`@@map`) comes from the model's resolved
42
+ * `tableName` independent of the list key/`modelName` so a renamed list
43
+ * key can still adopt a differently-named live table (e.g. better-auth's own
44
+ * default lowercase table names). When a `schema` is configured (plugin-level
45
+ * or per-model), the list is placed in that Postgres schema via `@@schema(...)`.
54
46
  *
55
- * With no `modelName`/`schema` overrides we emit only `timestamps: true`,
47
+ * With no `tableName`/`schema` overrides we emit only `timestamps: true`,
56
48
  * leaving the default `User`/`Session`/... table/schema output unchanged.
57
49
  */
58
- function listDb(model, defaultModelName) {
59
- const map = model.modelName !== defaultModelName ? model.modelName : undefined;
50
+ function listDb(model) {
60
51
  const schema = model.schema;
61
52
  return {
62
53
  timestamps: true,
63
- ...(map !== undefined ? { map } : {}),
54
+ ...(model.tableName !== undefined ? { map: model.tableName } : {}),
64
55
  ...(schema !== undefined ? { schema } : {}),
65
56
  };
66
57
  }
@@ -80,14 +71,18 @@ function fieldDb(fieldName, fields) {
80
71
  * Build the `db` config for a `user` relationship (`Session.user` /
81
72
  * `Account.user`), honouring a `userId` column override from the better-auth
82
73
  * `fields` map and mirroring better-auth's own FK shape: no separate FK index
83
- * — the index is applied at the field level via `isIndexed: false` — and
84
- * `onDelete: Cascade`, so a generated Auth schema diffs clean against a live
85
- * better-auth database on both dimensions instead of showing a spurious index
86
- * drop and a referential-action change (issue #679).
74
+ * — the index is applied at the field level via `isIndexed: false` —
75
+ * `onDelete: Cascade`, and a required (non-nullable) foreign key, since
76
+ * better-auth's adapter always writes a `userId` on every session/account row
77
+ * it creates. This means a generated Auth schema diffs clean against a live
78
+ * better-auth database on all three dimensions instead of showing a spurious
79
+ * index drop, a referential-action change, and a `DROP NOT NULL` (issues #679,
80
+ * #863).
87
81
  */
88
82
  function userRelationshipDb(fields) {
89
83
  const column = fields.userId;
90
84
  return {
85
+ isNullable: false,
91
86
  ...(column ? { foreignKey: { map: column } } : {}),
92
87
  extendPrismaSchema: ({ fkLine, relationLine }) => ({
93
88
  fkLine,
@@ -122,7 +117,7 @@ function createUserList(model, keys, userConfig, access) {
122
117
  // Custom fields from user config
123
118
  ...(userConfig.fields || {}),
124
119
  },
125
- db: listDb(model, DEFAULT_MODEL_NAMES.user),
120
+ db: listDb(model),
126
121
  access: userConfig.access || access,
127
122
  hooks: userConfig.hooks,
128
123
  });
@@ -142,7 +137,9 @@ function createSessionList(model, keys, access) {
142
137
  isIndexed: 'unique',
143
138
  db: fieldDb('token', f),
144
139
  }),
145
- expiresAt: timestamp({ db: fieldDb('expiresAt', f) }),
140
+ expiresAt: timestamp({
141
+ db: { isNullable: false, ...fieldDb('expiresAt', f) },
142
+ }),
146
143
  ipAddress: text({ db: fieldDb('ipAddress', f) }),
147
144
  userAgent: text({ db: fieldDb('userAgent', f) }),
148
145
  user: relationship({
@@ -151,7 +148,7 @@ function createSessionList(model, keys, access) {
151
148
  db: userRelationshipDb(f),
152
149
  }),
153
150
  },
154
- db: listDb(model, DEFAULT_MODEL_NAMES.session),
151
+ db: listDb(model),
155
152
  access,
156
153
  });
157
154
  }
@@ -180,7 +177,7 @@ function createAccountList(model, keys, access) {
180
177
  idToken: text({ db: fieldDb('idToken', f) }),
181
178
  password: text({ db: fieldDb('password', f) }),
182
179
  },
183
- db: listDb(model, DEFAULT_MODEL_NAMES.account),
180
+ db: listDb(model),
184
181
  access,
185
182
  });
186
183
  }
@@ -196,9 +193,11 @@ function createVerificationList(model, access) {
196
193
  fields: {
197
194
  identifier: text({ validation: { isRequired: true }, db: fieldDb('identifier', f) }),
198
195
  value: text({ validation: { isRequired: true }, db: fieldDb('value', f) }),
199
- expiresAt: timestamp({ db: fieldDb('expiresAt', f) }),
196
+ expiresAt: timestamp({
197
+ db: { isNullable: false, ...fieldDb('expiresAt', f) },
198
+ }),
200
199
  },
201
- db: listDb(model, DEFAULT_MODEL_NAMES.verification),
200
+ db: listDb(model),
202
201
  access,
203
202
  });
204
203
  }