@lenne.tech/nest-server 11.36.5 → 11.38.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 (101) hide show
  1. package/.claude/rules/better-auth.md +32 -0
  2. package/.claude/rules/configurable-features.md +29 -0
  3. package/.claude/rules/framework-compatibility.md +1 -0
  4. package/.claude/rules/module-deprecation.md +25 -1
  5. package/.claude/rules/testing.md +26 -4
  6. package/.claude/rules/versioning.md +6 -0
  7. package/CLAUDE.md +22 -5
  8. package/FRAMEWORK-API.md +4 -2
  9. package/dist/config.env.js +1 -1
  10. package/dist/config.env.js.map +1 -1
  11. package/dist/core/common/interfaces/server-options.interface.d.ts +20 -0
  12. package/dist/core/common/middlewares/security-headers.middleware.d.ts +18 -0
  13. package/dist/core/common/middlewares/security-headers.middleware.js +90 -0
  14. package/dist/core/common/middlewares/security-headers.middleware.js.map +1 -0
  15. package/dist/core/modules/auth/core-auth.controller.js +2 -5
  16. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  17. package/dist/core/modules/auth/core-auth.module.js +2 -0
  18. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  19. package/dist/core/modules/auth/core-auth.resolver.js +2 -5
  20. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  21. package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.d.ts +12 -0
  22. package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.js +83 -0
  23. package/dist/core/modules/auth/core-legacy-auth-deprecation.initializer.js.map +1 -0
  24. package/dist/core/modules/auth/helpers/legacy-endpoints.helper.d.ts +3 -0
  25. package/dist/core/modules/auth/helpers/legacy-endpoints.helper.js +14 -0
  26. package/dist/core/modules/auth/helpers/legacy-endpoints.helper.js.map +1 -0
  27. package/dist/core/modules/better-auth/better-auth.config.d.ts +12 -0
  28. package/dist/core/modules/better-auth/better-auth.config.js +33 -1
  29. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  30. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +9 -1
  31. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +51 -4
  32. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  33. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +2 -1
  34. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +29 -1
  35. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  36. package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.d.ts +2 -0
  37. package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.js +19 -0
  38. package/dist/core/modules/better-auth/core-better-auth-password-reset.registry.js.map +1 -0
  39. package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +4 -1
  40. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +15 -10
  41. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  42. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +4 -0
  43. package/dist/core/modules/better-auth/core-better-auth.constants.js +5 -1
  44. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -1
  45. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +8 -2
  46. package/dist/core/modules/better-auth/core-better-auth.module.js +34 -4
  47. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  48. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +2 -0
  49. package/dist/core/modules/better-auth/core-better-auth.service.js +68 -0
  50. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  51. package/dist/core/modules/system-setup/core-system-setup.service.js +3 -1
  52. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  53. package/dist/core/modules/user/core-user.service.d.ts +1 -1
  54. package/dist/core/modules/user/core-user.service.js +29 -12
  55. package/dist/core/modules/user/core-user.service.js.map +1 -1
  56. package/dist/core/modules/user/inputs/core-user.input.js +1 -1
  57. package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
  58. package/dist/core.module.js +2 -0
  59. package/dist/core.module.js.map +1 -1
  60. package/dist/index.d.ts +3 -0
  61. package/dist/index.js +3 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/server/modules/user/user.controller.js +2 -1
  64. package/dist/server/modules/user/user.controller.js.map +1 -1
  65. package/dist/server/modules/user/user.resolver.js +2 -1
  66. package/dist/server/modules/user/user.resolver.js.map +1 -1
  67. package/dist/server/modules/user/user.service.d.ts +1 -1
  68. package/dist/server/modules/user/user.service.js +8 -1
  69. package/dist/server/modules/user/user.service.js.map +1 -1
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/docs/REQUEST-LIFECYCLE.md +35 -1
  72. package/migration-guides/11.36.x-to-11.37.0.md +344 -0
  73. package/migration-guides/11.37.x-to-11.38.x.md +511 -0
  74. package/package.json +17 -4
  75. package/src/config.env.ts +9 -1
  76. package/src/core/common/interfaces/server-options.interface.ts +225 -9
  77. package/src/core/common/middlewares/security-headers.middleware.ts +155 -0
  78. package/src/core/modules/auth/README.md +104 -0
  79. package/src/core/modules/auth/core-auth.controller.ts +4 -11
  80. package/src/core/modules/auth/core-auth.module.ts +5 -0
  81. package/src/core/modules/auth/core-auth.resolver.ts +4 -11
  82. package/src/core/modules/auth/core-legacy-auth-deprecation.initializer.ts +128 -0
  83. package/src/core/modules/auth/helpers/legacy-endpoints.helper.ts +69 -0
  84. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +46 -1
  85. package/src/core/modules/better-auth/README.md +96 -59
  86. package/src/core/modules/better-auth/better-auth.config.ts +106 -4
  87. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +119 -3
  88. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +67 -3
  89. package/src/core/modules/better-auth/core-better-auth-password-reset.registry.ts +92 -0
  90. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +49 -10
  91. package/src/core/modules/better-auth/core-better-auth.constants.ts +26 -0
  92. package/src/core/modules/better-auth/core-better-auth.module.ts +67 -4
  93. package/src/core/modules/better-auth/core-better-auth.service.ts +179 -3
  94. package/src/core/modules/system-setup/core-system-setup.service.ts +33 -5
  95. package/src/core/modules/user/core-user.service.ts +123 -18
  96. package/src/core/modules/user/inputs/core-user.input.ts +16 -1
  97. package/src/core.module.ts +8 -0
  98. package/src/index.ts +3 -0
  99. package/src/server/modules/user/user.controller.ts +7 -1
  100. package/src/server/modules/user/user.resolver.ts +7 -1
  101. package/src/server/modules/user/user.service.ts +35 -9
@@ -78,7 +78,7 @@ JWT-based authentication for existing projects:
78
78
  | **Refresh Tokens** | Automatic token renewal |
79
79
  | **Sign In / Sign Up / Logout** | GraphQL mutations + REST endpoints |
80
80
  | **Rate Limiting** | Configurable per-endpoint rate limits (`auth.rateLimit`). Same `RateLimitStore` selection, async signatures and Redis-outage degradation as the BetterAuth row above (namespace `legacy-auth`) |
81
- | **Legacy Endpoint Controls** | Disable legacy endpoints after migration (`auth.legacyEndpoints`) |
81
+ | **Legacy Endpoint Controls** | Legacy endpoints are OFF unless `auth.legacyEndpoints.enabled: true` (default flipped in 11.38.0 — it was an opt-out, it is now an opt-in). `CoreLegacyAuthDeprecationInitializer` reports the state and the IAM migration progress at boot |
82
82
  | **Migration Tracking** | `betterAuthMigrationStatus` query for monitoring |
83
83
 
84
84
  #### Role System
@@ -592,6 +592,40 @@ If authentication succeeds, `req.user` is set with the authenticated user (inclu
592
592
  > raw-forwarded routes. A forged `POST /iam/update-user {"roles":["admin"]}` is rejected with
593
593
  > `FIELD_NOT_ALLOWED` (HTTP 400) at the input-parse stage, before any persistence.
594
594
 
595
+ > **"Raw" has one exception since 11.38.0: the password-setting reset routes.**
596
+ > `CoreBetterAuthApiMiddleware.normalizeResetPassword()` rewrites the password field of
597
+ > `/reset-password`, `/email-otp/reset-password` and `/phone-number/reset-password` to its
598
+ > normalized (sha256) form **before** building the Web Request, then runs the Better-Auth handler
599
+ > inside an `AsyncLocalStorage` context carrying that value.
600
+ >
601
+ > Both halves are load-bearing. Without the rewrite, a client posting a plaintext password would
602
+ > have `scrypt(plaintext)` stored while every sign-in — which *is* normalized, in
603
+ > `CoreBetterAuthController` — checks `scrypt(sha256(...))`: the account is locked out with the
604
+ > password its owner just chose. Without the context, `emailAndPassword.onPasswordReset` knows
605
+ > *which* user was reset but not to *what*, and cannot mirror the new password into the legacy
606
+ > bcrypt store — leaving the old password valid on the legacy path after a reset.
607
+ >
608
+ > A project subclassing the middleware must know this: the body it forwards is no longer
609
+ > byte-identical to the body it received. `normalizeResetPassword()` is `protected`.
610
+
611
+ #### 2b. SecurityHeadersMiddleware
612
+
613
+ Sets the browser security headers on **every** response — `X-Content-Type-Options`,
614
+ `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`, and the removal of
615
+ `X-Powered-By`. On by default; `security.headers: false` disables it, individual fields switch off
616
+ individually. No CSP is sent unless one is configured.
617
+
618
+ Middleware rather than an interceptor, and the distinction is the whole point: an interceptor never
619
+ runs for a request a guard turns away, and a `401` is still a response a browser renders. Those are
620
+ also the responses an attacker generates most of. `tests/security-headers.e2e-spec.ts` asserts the
621
+ headers on an actual guard rejection rather than trusting the ordering.
622
+
623
+ HSTS is decided by the request protocol — `x-forwarded-proto` first, connection protocol as
624
+ fallback — and never by configuration. A browser remembers it, so one sent over `http://localhost`
625
+ makes every project on that host unreachable over http for up to a year, with no server-side undo.
626
+ Behind a TLS-terminating proxy the inward connection is plain http, which makes `trustProxy`
627
+ load-bearing for this header too.
628
+
595
629
  #### 3. graphqlUploadExpress
596
630
 
597
631
  Only for GraphQL routes. Handles multipart file upload requests according to the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec).
@@ -0,0 +1,344 @@
1
+ # Migration Guide: 11.36.x → 11.37.0
2
+
3
+ > **This MINOR carries breaking changes.** That is not a slip. In this package the MAJOR digit
4
+ > tracks the NestJS major it targets — 11.x is NestJS 11 — so it moves only when NestJS does.
5
+ > Breaking changes of our own ship in a minor, which is why every one of them is spelled out
6
+ > below. Read §1 before you upgrade — and do not wait for `pnpm install` to tell you: on pnpm's
7
+ > defaults it will quietly install the new peers for you, at a version you never pinned.
8
+
9
+ ## Overview
10
+
11
+ | Category | Details |
12
+ |----------|---------|
13
+ | **Breaking Changes** | **`better-auth` is no longer a dependency of this package — it is a peer dependency you must declare yourself** (§1). It moves to **1.7.x** and cannot be held back (§2). `@better-auth/core` joins the contract as a third required peer (§1) |
14
+ | **New Features** | `account.issuer` is backfilled automatically on the first boot after the upgrade, so existing password users keep signing in (§3) |
15
+ | **Bugfixes** | Legacy→IAM account migration wrote an account better-auth 1.7 could not find, which turned the very sign-in that triggered the migration into a 401 (§3) |
16
+ | **Migration Effort** | **Every project: one package.json change (§1) and one deployment note (§3).** Add ~15 minutes if you use social or SSO logins (§4), and read §5 before running two nest-server versions against one database |
17
+
18
+ ---
19
+
20
+ ## Quick Migration
21
+
22
+ ```bash
23
+ pnpm add better-auth@1.7.1 @better-auth/passkey@1.7.1 @better-auth/core@1.7.1
24
+ pnpm add @lenne.tech/nest-server@11.37.0
25
+ pnpm run build
26
+ ```
27
+
28
+ Then read §3 — it concerns your **data**, not your code, and no build will tell you about it.
29
+
30
+ ---
31
+
32
+ ## 1. `better-auth` is now a peer dependency
33
+
34
+ **Were you affected?** Every project — and the failure mode depends on your package manager,
35
+ which is why you cannot rely on the install to flag it.
36
+
37
+ | Setting | What `pnpm install` does |
38
+ |---|---|
39
+ | `autoInstallPeers: true` (**pnpm's default**) | installs the peers **silently**, resolving anywhere inside `>=1.7.1 <1.8.0` |
40
+ | `autoInstallPeers: false` | fails with a missing-peer error |
41
+ | `strictPeerDependencies: true` | fails on a version outside the range |
42
+
43
+ Most projects are on the default, so a green install proves nothing: you end up with an
44
+ unpinned better-auth that can drift on the next install — exactly the split this section exists
45
+ to prevent. Declare all three yourself, pinned, and verify with:
46
+
47
+ ```bash
48
+ pnpm why better-auth # must show YOUR declaration, not just a peer-resolved copy
49
+ ```
50
+
51
+ ### What changed
52
+
53
+ Up to 11.36.x, `package.json` carried better-auth as an exactly pinned **dependency**:
54
+
55
+ ```jsonc
56
+ "dependencies": {
57
+ "better-auth": "1.6.26",
58
+ "@better-auth/passkey": "1.6.26"
59
+ }
60
+ ```
61
+
62
+ From 11.37.0 they are **peer dependencies**, and `@better-auth/core` joins them:
63
+
64
+ ```jsonc
65
+ "peerDependencies": {
66
+ "better-auth": ">=1.7.0 <1.8.0",
67
+ "@better-auth/passkey": ">=1.7.0 <1.8.0",
68
+ "@better-auth/core": ">=1.7.0 <1.8.0"
69
+ }
70
+ ```
71
+
72
+ All three are **non-optional**. That is deliberate and differs from the five peers this package
73
+ already had (`ioredis`, `bullmq`, `@aws-sdk/*`, `@tus/s3-store`), which are `optional: true` and
74
+ lazily imported. better-auth is imported statically at the top of
75
+ `src/core/modules/better-auth/better-auth.config.ts` — if it is missing, the server does not
76
+ degrade, it fails to boot.
77
+
78
+ Add them to your own `package.json`, exactly pinned:
79
+
80
+ ```jsonc
81
+ "dependencies": {
82
+ "better-auth": "1.7.1",
83
+ "@better-auth/passkey": "1.7.1",
84
+ "@better-auth/core": "1.7.1"
85
+ }
86
+ ```
87
+
88
+ ### Why
89
+
90
+ An exact pin inside a library makes the library the owner of that version. A project could not
91
+ raise better-auth without overriding this package, and in a fullstack setup that is fatal: the
92
+ frontend (`@lenne.tech/nuxt-extensions`) has always declared better-auth as a **peer**, so the app
93
+ could move to 1.7 while the api was pinned to 1.6.26 — a client and a server speaking different
94
+ versions of the same protocol. See §2 for what that actually broke.
95
+
96
+ The version now belongs to the project, which is the only place that can keep client and server in
97
+ step. The narrow range is not overcaution: better-auth breaks in **minor** releases (1.7 removed
98
+ the `./plugins/oidc-provider` and `./plugins/mcp/client` subpath exports and changed the 2FA
99
+ response shape), so `^1.7.0` would invite the same split at 1.8.
100
+
101
+ ### Vendor mode
102
+
103
+ Vendor-mode projects already carry better-auth in their own `package.json` — that is what vendoring
104
+ means — so the *mechanism* does not change. The work does:
105
+
106
+ - bump `better-auth` and `@better-auth/passkey` from `1.6.26` to `1.7.1`, and
107
+ - **add `@better-auth/core`**, which has never been in your manifest.
108
+
109
+ `/lt-dev:backend:update-nest-server-core` rewrites `src/core/**` and does **not** touch
110
+ `package.json` — these three lines are yours to change. For a NEW vendor-mode project, the `lt`
111
+ CLI must be new enough to carry all three in `src/config/vendor-runtime-deps.json`; an older CLI
112
+ vendors a core that imports packages the generated manifest never declares.
113
+
114
+ ---
115
+
116
+ ## 2. better-auth moves to 1.7.x
117
+
118
+ **Were you affected?** Every project, and **fullstack projects most of all**.
119
+
120
+ `>=1.7.0 <1.8.0` has no 1.6 in it. This is not a version you can defer.
121
+
122
+ ### What this fixes
123
+
124
+ better-auth 1.7 gives `twoFactor.enable` a discriminated result carrying
125
+ `method: "otp" | "totp"`. A 1.7 client narrows on that discriminant before reading `totpURI` and
126
+ `backupCodes`. A 1.6.26 server never sends the field, so the narrowing fails and **every 2FA
127
+ activation is rejected** — with a generic error, against a server that logs nothing unusual.
128
+
129
+ If your app is on `@lenne.tech/nuxt-extensions` with better-auth 1.7 and your api was still on
130
+ 11.36.x, that is what your users were hitting.
131
+
132
+ ### What it costs
133
+
134
+ Two API changes in better-auth 1.7 reach code you may have written yourself:
135
+
136
+ - `context.internalAdapter.createUser(user)` now takes a second argument:
137
+ `createUser(user, { method })`, where `method` is the provisioning source
138
+ (`'email-password' | 'admin' | 'oauth' | …`).
139
+ - `context.internalAdapter.linkAccount()` requires `issuer` — see §3.
140
+
141
+ Both are compile errors, so `pnpm run build` finds them. Anything writing to the `account`
142
+ collection **directly** will not be caught by the compiler — see §3.
143
+
144
+ ---
145
+
146
+ ## 3. `account.issuer`: the part no build will tell you about
147
+
148
+ **Were you affected?** Every project with existing users. This is a **data** change.
149
+
150
+ ### What changed
151
+
152
+ Up to 1.6 a credential account was identified by `providerId` + `userId`. From 1.7 an account is
153
+ keyed by **(issuer, accountId)**, and the sign-in route filters on it verbatim:
154
+
155
+ ```js
156
+ account.providerId === 'credential' && account.issuer === credentialIssuer && account.accountId === user.id
157
+ ```
158
+
159
+ Every account row written by better-auth 1.6 has **no `issuer` field at all**. `undefined` never
160
+ equals `'local:credential'`, so after the upgrade better-auth cannot find those accounts: **every
161
+ existing password user is locked out**, with a 401 that says nothing about why.
162
+
163
+ ### What this package does about it
164
+
165
+ On the first boot after the upgrade, `CoreBetterAuthService.onModuleInit()` backfills the field:
166
+
167
+ ```js
168
+ db.collection('account').updateMany(
169
+ { issuer: { $exists: false }, providerId: 'credential' },
170
+ { $set: { issuer: createLocalAccountIssuer('credential') } },
171
+ )
172
+ ```
173
+
174
+ It is idempotent — the filter only matches rows still missing the field — and it logs how many rows
175
+ it touched. If it fails, it logs an **error** and the server still starts, because a server that
176
+ boots with a loud error beats one that will not boot at all. Check your logs for
177
+ `Could not backfill account.issuer` after the first deployment.
178
+
179
+ Credential accounts only. See §4 for why.
180
+
181
+ ### If you write to the `account` collection yourself
182
+
183
+ Any code that inserts an account row directly through the Mongo driver bypasses better-auth's types
184
+ and therefore the compiler. It must now set the issuer:
185
+
186
+ ```diff
187
+ await accountsCollection.insertOne({
188
+ accountId: userIdHex,
189
+ + issuer: createLocalAccountIssuer('credential'),
190
+ password: passwordHash,
191
+ providerId: 'credential',
192
+ userId: userMongoId,
193
+ });
194
+ ```
195
+
196
+ ```ts
197
+ import { createLocalAccountIssuer } from '@better-auth/core/db';
198
+ ```
199
+
200
+ **Derive it, never hand-write `'local:credential'`.** The format is better-auth's to change, and a
201
+ literal copy would keep compiling while silently no longer matching the accounts better-auth writes
202
+ itself.
203
+
204
+ This bug was live in this package: `CoreBetterAuthUserMapper.migrateAccountToIam()` wrote a
205
+ credential account without the issuer, so a legacy user migrating to IAM got a 401 on the very
206
+ sign-in that triggered the migration. It is fixed in 11.37.0, and 12 e2e tests that were red on
207
+ better-auth 1.7 pin it.
208
+
209
+ ---
210
+
211
+ ## 4. Social and SSO logins are NOT backfilled
212
+
213
+ **Were you affected?** Only projects using OAuth or SSO providers — but for those this is a
214
+ **pre-upgrade** step, not a post-upgrade cleanup. See the numbered consequences below.
215
+
216
+ The backfill deliberately stops at credential accounts. `local:credential` is a pure function of
217
+ the provider id and therefore derivable. An OAuth account's issuer is not: it is either the
218
+ provider's **real OIDC issuer** or the synthetic `local:oauth:<providerId>` fallback, decided per
219
+ provider and overridable through the provider config.
220
+
221
+ Guessing there would not fail loudly. It would write a key that looks valid, and produce a **second
222
+ account** for the same user on the next social sign-in.
223
+
224
+ So those rows are counted and reported instead. If your logs say
225
+
226
+ ```
227
+ At least one non-credential account has no "issuer"
228
+ ```
229
+
230
+ then act **before** the first social sign-in after the upgrade. Such a sign-in does **not** simply
231
+ fail, which is the part that makes this urgent rather than merely broken:
232
+
233
+ 1. better-auth cannot match the row by `(issuer, accountId)`, so it falls back to matching the
234
+ user by the **provider-asserted email** and implicitly links a **second** account row. The
235
+ identity is now established from an email rather than from the provider key — a weaker binding.
236
+ 2. If the provider-side email has changed since the row was written, no user matches and
237
+ better-auth creates a **new user**. The original account is orphaned with its data.
238
+ 3. The orphaned row keeps its `accessToken` / `refreshToken`. Unlinking removes the *new* row, so
239
+ those provider credentials survive every unlink and every "revoke access" action.
240
+
241
+ For a provider using the synthetic fallback:
242
+
243
+ ```js
244
+ db.collection('account').updateMany(
245
+ { issuer: { $exists: false }, providerId: 'google' },
246
+ { $set: { issuer: 'local:oauth:google' } },
247
+ )
248
+ ```
249
+
250
+ For a provider with a real OIDC issuer, use that issuer verbatim. Confirm which of the two applies
251
+ by reading a row better-auth wrote itself after the upgrade.
252
+
253
+ ---
254
+
255
+ ## 5. Deploying: do not run 11.36.x and 11.37.x against one database
256
+
257
+ The backfill is safe to run repeatedly and safe to run while 11.37.x instances serve traffic. It is
258
+ **not** safe to leave an 11.36.x instance running against the same database afterwards: 11.x keeps
259
+ writing credential accounts without an issuer, and those rows are invisible to 11.37.x until the next
260
+ restart backfills them.
261
+
262
+ For a rolling deployment, either complete the rollout before relying on new sign-ups, or restart
263
+ one 11.37.x instance after the last 11.36.x instance is gone.
264
+
265
+ **Rollback** is safe in the other direction: 1.6 ignores the extra `issuer` field, so a backfilled
266
+ database still works with 11.36.x.
267
+
268
+ ---
269
+
270
+ ## 6. Compatibility Notes
271
+
272
+ Patterns this release does and does not disturb. All of these are override points projects
273
+ legitimately use.
274
+
275
+ | Pattern | Status | Notes |
276
+ |---|---|---|
277
+ | Subclassing `CoreBetterAuthService` | Compatible | `backfillAccountIssuers()` and `ensureIndices()` are `protected` — override either to change behaviour |
278
+ | **Overriding `onModuleInit()`** | **Action required** | It now runs two steps. An override that does not call `super.onModuleInit()` silently skips the backfill, and your existing password users stay locked out with no error anywhere. Call `super`, or call both `protected` methods yourself |
279
+ | Subclassing `CoreSystemSetupService` | Compatible | See §7 if you override `createInitialAdmin()` — it now passes a provisioning source |
280
+ | Subclassing `CoreBetterAuthUserMapper` | Compatible | An override of `migrateAccountToIam()` that builds the account document itself MUST add `issuer: createLocalAccountIssuer('credential')`, or the migrated user gets a 401 on the sign-in that triggered the migration |
281
+ | Custom `account` collection name / `issuer` field | Compatible | The backfill resolves both from the running better-auth instance and warns when they differ from the defaults |
282
+ | `betterAuth.options.user.validateUserInfo` | **Action required** | See §7 |
283
+ | Reading the `account` collection directly | Compatible | Framework reads deliberately filter on `providerId` alone, so they keep working on un-backfilled rows. Do not add `issuer` to such a filter |
284
+
285
+ ---
286
+
287
+ ## 7. `validateUserInfo` and the initial admin
288
+
289
+ **Were you affected?** Only projects that configure `betterAuth.options.user.validateUserInfo`.
290
+
291
+ better-auth 1.7 requires a *provisioning source* on `internalAdapter.createUser()`, and
292
+ `CoreSystemSetupService` now passes `{ method: 'admin' }`. Two consequences:
293
+
294
+ 1. Your hook still runs — the source does not bypass it. It receives
295
+ `{ method: 'admin', action: 'create-user' }`, so a domain allowlist or invite-code gate can
296
+ branch on `method` and let the first-admin provisioning through.
297
+ 2. **When the hook is configured, initial-admin setup fails.** better-auth additionally calls
298
+ `getCurrentAuthContext()`, which throws outside an endpoint context. System setup runs from
299
+ `OnApplicationBootstrap` or a plain controller — never inside better-auth's request pipeline —
300
+ so the call ends in `FORBIDDEN / validation_context_missing` and no admin is created.
301
+
302
+ If you use `validateUserInfo`, provision the first administrator another way: create it through a
303
+ request-scoped IAM route, or seed it before enabling the hook.
304
+
305
+ ---
306
+
307
+ ## 8. Troubleshooting
308
+
309
+ | Symptom | Cause | Fix |
310
+ |---|---|---|
311
+ | **Every password user gets 401 after the upgrade.** Correct password, no useful log line | Their `account` rows predate better-auth 1.7 and carry no `issuer` | The backfill runs on the first boot. Check the log for `Backfilled account.issuer on N credential account(s)`. If absent, see the next two rows |
312
+ | No backfill log at all, and users still cannot sign in | The backfill did not run: better-auth disabled, no DB connection, or an `onModuleInit()` override without `super` (§6) | Check `betterAuth.enabled`, then your override |
313
+ | `Backfilled N/M credential accounts. X user(s) CANNOT sign in` | The bulk write hit a duplicate `(issuer, accountId)` pair, usually from an earlier partial migration | Find them: `db.account.aggregate([{$group:{_id:{i:'$issuer',a:'$accountId'},n:{$sum:1}}},{$match:{n:{$gt:1}}}])`, resolve the duplicates, restart |
314
+ | `Backfilling the account issuer against a customised schema` | You renamed the account model or the issuer field | Expected. Verify the names match what better-auth writes |
315
+ | `Could not backfill the account issuer: …` | The database rejected the operation | Boot is not blocked, but affected users cannot sign in. Fix the cause and restart — the backfill retries until it completes |
316
+ | Social login creates a second account, or a brand-new empty user | An OAuth row without an issuer (§4) | Set the issuer per provider. The already-created duplicate must be merged manually |
317
+ | Setup fails with `FORBIDDEN / validation_context_missing` | `validateUserInfo` is configured (§7) | Provision the first admin through a request-scoped path |
318
+ | `pnpm install` succeeded but versions drift later | pnpm auto-installed the peers (§1) | Declare all three explicitly and pin them; verify with `pnpm why better-auth` |
319
+
320
+ ---
321
+
322
+ ## Module Documentation
323
+
324
+ | Document | Relevance |
325
+ |---|---|
326
+ | [`src/core/modules/better-auth/README.md`](../src/core/modules/better-auth/README.md) | Module overview, the boot-time backfill, troubleshooting |
327
+ | [`src/core/modules/better-auth/INTEGRATION-CHECKLIST.md`](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md) | Integration steps — **step 0 (installing the three peers) is new in 11.37.0** |
328
+ | [`src/core/modules/better-auth/CUSTOMIZATION.md`](../src/core/modules/better-auth/CUSTOMIZATION.md) | Registration patterns and override points |
329
+ | [`src/core/modules/system-setup/README.md`](../src/core/modules/system-setup/README.md) | Initial-admin provisioning (§7) |
330
+ | [`.claude/rules/better-auth.md`](../.claude/rules/better-auth.md) | Development rules for the module |
331
+
332
+ ---
333
+
334
+ ## Checklist
335
+
336
+ - [ ] `better-auth`, `@better-auth/passkey`, `@better-auth/core` declared in your own `package.json`, pinned to the same version (§1)
337
+ - [ ] `pnpm run build` green — it finds the `createUser` and `linkAccount` signature changes (§2)
338
+ - [ ] Every direct write to the `account` collection sets `issuer` via `createLocalAccountIssuer` (§3)
339
+ - [ ] First boot after deployment checked for `Backfilled account.issuer` and for `Could not backfill` (§3)
340
+ - [ ] Social/SSO projects: log checked for un-backfilled accounts, issuer set per provider (§4)
341
+ - [ ] Fullstack projects: api and app on the **same** better-auth version (§1)
342
+ - [ ] Rollout does not leave 11.36.x and 11.37.x on one database (§5)
343
+ - [ ] `onModuleInit()` overrides call `super.onModuleInit()` (§6)
344
+ - [ ] Projects using `validateUserInfo`: first-admin provisioning path checked (§7)