@lenne.tech/nest-server 11.21.2 → 11.22.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 (71) hide show
  1. package/.claude/rules/architecture.md +79 -0
  2. package/.claude/rules/better-auth.md +262 -0
  3. package/.claude/rules/configurable-features.md +308 -0
  4. package/.claude/rules/core-modules.md +205 -0
  5. package/.claude/rules/migration-guides.md +149 -0
  6. package/.claude/rules/module-deprecation.md +214 -0
  7. package/.claude/rules/module-inheritance.md +97 -0
  8. package/.claude/rules/package-management.md +112 -0
  9. package/.claude/rules/role-system.md +146 -0
  10. package/.claude/rules/testing.md +120 -0
  11. package/.claude/rules/versioning.md +53 -0
  12. package/CLAUDE.md +172 -0
  13. package/dist/core/common/interfaces/server-options.interface.d.ts +10 -0
  14. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +25 -25
  15. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  16. package/dist/core/modules/better-auth/core-better-auth.service.js +8 -4
  17. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  18. package/dist/core/modules/error-code/error-code.module.js.map +1 -1
  19. package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
  20. package/dist/core/modules/tenant/core-tenant.guard.js +59 -4
  21. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  22. package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
  23. package/dist/core.module.d.ts +3 -3
  24. package/dist/core.module.js +17 -4
  25. package/dist/core.module.js.map +1 -1
  26. package/dist/server/server.module.js +6 -6
  27. package/dist/server/server.module.js.map +1 -1
  28. package/dist/test/test.helper.d.ts +6 -2
  29. package/dist/test/test.helper.js +28 -6
  30. package/dist/test/test.helper.js.map +1 -1
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/docs/REQUEST-LIFECYCLE.md +1256 -0
  33. package/docs/error-codes.md +446 -0
  34. package/migration-guides/11.10.x-to-11.11.x.md +266 -0
  35. package/migration-guides/11.11.x-to-11.12.x.md +323 -0
  36. package/migration-guides/11.12.x-to-11.13.0.md +612 -0
  37. package/migration-guides/11.13.x-to-11.14.0.md +348 -0
  38. package/migration-guides/11.14.x-to-11.15.0.md +262 -0
  39. package/migration-guides/11.15.0-to-11.15.3.md +118 -0
  40. package/migration-guides/11.15.x-to-11.16.0.md +497 -0
  41. package/migration-guides/11.16.x-to-11.17.0.md +130 -0
  42. package/migration-guides/11.17.x-to-11.18.0.md +393 -0
  43. package/migration-guides/11.18.x-to-11.19.0.md +151 -0
  44. package/migration-guides/11.19.x-to-11.20.0.md +170 -0
  45. package/migration-guides/11.20.x-to-11.21.0.md +216 -0
  46. package/migration-guides/11.21.0-to-11.21.1.md +194 -0
  47. package/migration-guides/11.21.1-to-11.21.2.md +114 -0
  48. package/migration-guides/11.21.2-to-11.21.3.md +175 -0
  49. package/migration-guides/11.21.x-to-11.22.0.md +224 -0
  50. package/migration-guides/11.3.x-to-11.4.x.md +233 -0
  51. package/migration-guides/11.6.x-to-11.7.x.md +394 -0
  52. package/migration-guides/11.7.x-to-11.8.x.md +318 -0
  53. package/migration-guides/11.8.x-to-11.9.x.md +322 -0
  54. package/migration-guides/11.9.x-to-11.10.x.md +571 -0
  55. package/migration-guides/TEMPLATE.md +113 -0
  56. package/package.json +8 -3
  57. package/src/core/common/interfaces/server-options.interface.ts +83 -16
  58. package/src/core/modules/better-auth/CUSTOMIZATION.md +24 -17
  59. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +5 -5
  60. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +29 -25
  61. package/src/core/modules/better-auth/core-better-auth.service.ts +13 -9
  62. package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +42 -12
  63. package/src/core/modules/error-code/error-code.module.ts +4 -9
  64. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +13 -2
  65. package/src/core/modules/tenant/README.md +26 -1
  66. package/src/core/modules/tenant/core-tenant.guard.ts +142 -11
  67. package/src/core/modules/tenant/core-tenant.helpers.ts +6 -2
  68. package/src/core.module.ts +52 -10
  69. package/src/server/server.module.ts +7 -9
  70. package/src/test/README.md +47 -0
  71. package/src/test/test.helper.ts +55 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.21.2",
3
+ "version": "11.22.0",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -168,7 +168,11 @@
168
168
  "files": [
169
169
  "dist/**/*",
170
170
  "src/**/*",
171
- "bin/**/*"
171
+ "bin/**/*",
172
+ "CLAUDE.md",
173
+ ".claude/rules/**/*",
174
+ "docs/**/*",
175
+ "migration-guides/**/*"
172
176
  ],
173
177
  "watch": {
174
178
  "build:dev": "src"
@@ -191,7 +195,8 @@
191
195
  "picomatch@>=4.0.0 <4.0.4": "4.0.4",
192
196
  "path-to-regexp@>=8.0.0 <8.4.0": "8.4.1",
193
197
  "kysely@>=0.26.0 <0.28.15": "0.28.15",
194
- "lodash@>=4.0.0 <4.18.0": "4.18.1"
198
+ "lodash@>=4.0.0 <4.18.0": "4.18.1",
199
+ "defu@<=6.1.4": "6.1.6"
195
200
  },
196
201
  "onlyBuiltDependencies": [
197
202
  "bcrypt",
@@ -2117,16 +2117,12 @@ interface IBetterAuthBase {
2117
2117
  * Custom controller class to use instead of the default CoreBetterAuthController.
2118
2118
  * The class should extend CoreBetterAuthController.
2119
2119
  *
2120
- * This allows projects to customize REST endpoints via config instead of creating
2121
- * a separate module. Use this with CoreModule.forRoot(envConfig) (IAM-only mode).
2122
- *
2123
- * @example
2120
+ * @deprecated Since 11.22.0 Use the `overrides` parameter on `CoreModule.forRoot()` instead:
2124
2121
  * ```typescript
2125
- * // config.env.ts
2126
- * betterAuth: {
2127
- * controller: IamController,
2128
- * }
2122
+ * CoreModule.forRoot(envConfig, { betterAuth: { controller: IamController } })
2129
2123
  * ```
2124
+ * This separates class references from environment configuration. The config field
2125
+ * still works for backward compatibility but `overrides` takes precedence.
2130
2126
  *
2131
2127
  * @since 11.14.0
2132
2128
  */
@@ -2342,16 +2338,12 @@ interface IBetterAuthBase {
2342
2338
  * Custom resolver class to use instead of the default DefaultBetterAuthResolver.
2343
2339
  * The class should extend CoreBetterAuthResolver.
2344
2340
  *
2345
- * This allows projects to customize GraphQL operations via config instead of creating
2346
- * a separate module. Use this with CoreModule.forRoot(envConfig) (IAM-only mode).
2347
- *
2348
- * @example
2341
+ * @deprecated Since 11.22.0 Use the `overrides` parameter on `CoreModule.forRoot()` instead:
2349
2342
  * ```typescript
2350
- * // config.env.ts
2351
- * betterAuth: {
2352
- * resolver: IamResolver,
2353
- * }
2343
+ * CoreModule.forRoot(envConfig, { betterAuth: { resolver: IamResolver } })
2354
2344
  * ```
2345
+ * This separates class references from environment configuration. The config field
2346
+ * still works for backward compatibility but `overrides` takes precedence.
2355
2347
  *
2356
2348
  * @since 11.14.0
2357
2349
  */
@@ -2663,3 +2655,78 @@ interface IBetterAuthWithPasskey extends IBetterAuthBase {
2663
2655
  */
2664
2656
  trustedOrigins: string[];
2665
2657
  }
2658
+
2659
+ /**
2660
+ * Override default implementations of core module components.
2661
+ *
2662
+ * Use this as the second parameter of `CoreModule.forRoot()` to replace
2663
+ * default controllers, resolvers, or services with project-specific implementations.
2664
+ *
2665
+ * This keeps class references (code) separate from environment configuration (strings/numbers)
2666
+ * and ensures each module's `forRoot()` is called exactly once — preventing duplicate
2667
+ * controller registration.
2668
+ *
2669
+ * Each core module that supports customization has a typed entry here.
2670
+ * Only specified components are overridden — unset fields use defaults.
2671
+ *
2672
+ * @example
2673
+ * ```typescript
2674
+ * // IAM-only mode with overrides
2675
+ * CoreModule.forRoot(envConfig, {
2676
+ * errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
2677
+ * betterAuth: { resolver: BetterAuthResolver },
2678
+ * })
2679
+ *
2680
+ * // Legacy mode with overrides
2681
+ * CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig, {
2682
+ * errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
2683
+ * betterAuth: { controller: BetterAuthController, resolver: BetterAuthResolver },
2684
+ * })
2685
+ * ```
2686
+ *
2687
+ * @since 11.22.0
2688
+ */
2689
+ export interface ICoreModuleOverrides {
2690
+ /**
2691
+ * Override BetterAuth controller and/or resolver.
2692
+ *
2693
+ * The custom controller must extend `CoreBetterAuthController`.
2694
+ * The custom resolver must extend `CoreBetterAuthResolver` and re-declare
2695
+ * all GraphQL decorators (`@Mutation`, `@Query`, `@Roles`).
2696
+ *
2697
+ * @example
2698
+ * ```typescript
2699
+ * {
2700
+ * betterAuth: {
2701
+ * controller: BetterAuthController,
2702
+ * resolver: BetterAuthResolver,
2703
+ * },
2704
+ * }
2705
+ * ```
2706
+ */
2707
+ betterAuth?: {
2708
+ controller?: Type<any>;
2709
+ resolver?: Type<any>;
2710
+ };
2711
+
2712
+ /**
2713
+ * Override ErrorCode controller and/or service.
2714
+ *
2715
+ * The custom controller can be standalone (recommended) or extend `CoreErrorCodeController`.
2716
+ * The custom service must extend `CoreErrorCodeService`.
2717
+ *
2718
+ * @example
2719
+ * ```typescript
2720
+ * {
2721
+ * errorCode: {
2722
+ * controller: ErrorCodeController,
2723
+ * service: ErrorCodeService,
2724
+ * },
2725
+ * }
2726
+ * ```
2727
+ */
2728
+ errorCode?: {
2729
+ controller?: Type<any>;
2730
+ service?: Type<any>;
2731
+ };
2732
+ }
@@ -41,29 +41,23 @@ export class ServerModule {}
41
41
  - Default `CoreBetterAuthController` and `DefaultBetterAuthResolver` are registered
42
42
  - No additional configuration needed
43
43
 
44
- ### Pattern 2: Config-based Controller/Resolver (Recommended for Customization)
44
+ ### Pattern 2: Overrides Parameter (Recommended for Customization)
45
45
 
46
46
  **Use when:** Need custom Controller or Resolver, but don't need a separate module.
47
47
 
48
- ```typescript
49
- // config.env.ts
50
- import { IamController } from './server/modules/iam/iam.controller';
51
- import { IamResolver } from './server/modules/iam/iam.resolver';
52
-
53
- const config = {
54
- betterAuth: {
55
- controller: IamController, // Custom controller class
56
- resolver: IamResolver, // Custom resolver class
57
- // ... other betterAuth config
58
- },
59
- };
60
- ```
61
-
62
48
  ```typescript
63
49
  // server.module.ts
50
+ import { IamController } from './modules/iam/iam.controller';
51
+ import { IamResolver } from './modules/iam/iam.resolver';
52
+
64
53
  @Module({
65
54
  imports: [
66
- CoreModule.forRoot(envConfig), // Uses custom controller/resolver from config
55
+ CoreModule.forRoot(envConfig, {
56
+ betterAuth: {
57
+ controller: IamController,
58
+ resolver: IamResolver,
59
+ },
60
+ }),
67
61
  ],
68
62
  })
69
63
  export class ServerModule {}
@@ -71,9 +65,22 @@ export class ServerModule {}
71
65
 
72
66
  **What happens:**
73
67
 
74
- - CoreModule passes `controller`/`resolver` to `CoreBetterAuthModule.forRoot()`
68
+ - CoreModule passes overrides to `CoreBetterAuthModule.forRoot()`
75
69
  - Your custom classes are registered instead of defaults
76
70
  - Single registration point, no duplicate imports
71
+ - Class references stay separate from environment config
72
+
73
+ **Alternative:** Set `controller`/`resolver` directly in config (backward compatible):
74
+
75
+ ```typescript
76
+ // config.env.ts — still works but overrides parameter is preferred
77
+ const config = {
78
+ betterAuth: {
79
+ controller: IamController,
80
+ resolver: IamResolver,
81
+ },
82
+ };
83
+ ```
77
84
 
78
85
  ### Pattern 3: Separate Module (autoRegister: false)
79
86
 
@@ -23,11 +23,11 @@
23
23
 
24
24
  ### Registration Patterns (Quick Reference)
25
25
 
26
- | Pattern | Use When | Configuration |
27
- | ------------------- | ---------------------------------- | -------------------------------------------------- |
28
- | **Zero-Config** | No customization needed | Just use `CoreModule.forRoot(envConfig)` |
29
- | **Config-based** | Custom Controller/Resolver | Add `controller`/`resolver` to `betterAuth` config |
30
- | **Separate Module** | Full control, additional providers | Set `autoRegister: false` in config |
26
+ | Pattern | Use When | Configuration |
27
+ | --------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------- |
28
+ | **Zero-Config** | No customization needed | Just use `CoreModule.forRoot(envConfig)` |
29
+ | **Overrides** (recommended) | Custom Controller/Resolver | Pass `overrides` to `CoreModule.forRoot(envConfig, { betterAuth: { controller, resolver } })` |
30
+ | **Separate Module** | Full control, additional providers | Set `autoRegister: false` in config |
31
31
 
32
32
  **Details:** See [CUSTOMIZATION.md](./CUSTOMIZATION.md#module-registration-patterns)
33
33
 
@@ -1019,32 +1019,36 @@ export class CoreBetterAuthUserMapper {
1019
1019
  const migrationPercentage = totalUsers > 0 ? Math.round((fullyMigratedUsers / totalUsers) * 100 * 100) / 100 : 0;
1020
1020
 
1021
1021
  // Get emails of pending users (limit to 100)
1022
- const pendingUsers = await usersCollection
1023
- .aggregate([
1024
- {
1025
- $lookup: {
1026
- as: 'accounts',
1027
- foreignField: 'userId',
1028
- from: 'account',
1029
- localField: '_id',
1030
- },
1031
- },
1032
- {
1033
- $match: {
1034
- $or: [
1035
- { iamId: { $exists: false } },
1036
- { iamId: null },
1037
- {
1038
- $and: [{ iamId: { $exists: true, $ne: null } }, { 'accounts.providerId': { $ne: 'credential' } }],
1039
- },
1040
- ],
1041
- },
1042
- },
1043
- { $limit: 100 },
1044
- { $project: { email: 1 } },
1045
- ])
1022
+ // Two-phase approach: first get users without iamId (no $lookup needed),
1023
+ // then check users with iamId but missing credential account
1024
+ const usersWithoutIamId = await usersCollection
1025
+ .find({ $or: [{ iamId: { $exists: false } }, { iamId: null }] })
1026
+ .limit(100)
1027
+ .project({ email: 1 })
1046
1028
  .toArray();
1047
- const pendingUserEmails = pendingUsers.map((u) => u.email).filter(Boolean);
1029
+
1030
+ const remaining = 100 - usersWithoutIamId.length;
1031
+ let usersWithIamButNoAccount: { email?: string }[] = [];
1032
+ if (remaining > 0) {
1033
+ usersWithIamButNoAccount = await usersCollection
1034
+ .aggregate([
1035
+ { $match: { iamId: { $exists: true, $ne: null } } },
1036
+ {
1037
+ $lookup: {
1038
+ as: 'accounts',
1039
+ foreignField: 'userId',
1040
+ from: 'account',
1041
+ localField: '_id',
1042
+ },
1043
+ },
1044
+ { $match: { 'accounts.providerId': { $ne: 'credential' } } },
1045
+ { $limit: remaining },
1046
+ { $project: { email: 1 } },
1047
+ ])
1048
+ .toArray();
1049
+ }
1050
+
1051
+ const pendingUserEmails = [...usersWithoutIamId, ...usersWithIamButNoAccount].map((u) => u.email).filter(Boolean);
1048
1052
 
1049
1053
  // Can disable legacy auth only if ALL users are fully migrated
1050
1054
  const canDisableLegacyAuth = totalUsers > 0 && fullyMigratedUsers === totalUsers;
@@ -88,15 +88,19 @@ export class CoreBetterAuthService implements OnModuleInit {
88
88
  try {
89
89
  const db = this.connection.db;
90
90
 
91
- // Session collection: token lookup (getSessionByToken) and user+expiry lookup (getActiveSessionForUser)
92
- await db.collection('session').createIndex({ token: 1 });
93
- await db.collection('session').createIndex({ userId: 1, expiresAt: 1 });
94
-
95
- // Users collection: iamId lookup (mapSessionUser uses $or with email and iamId)
96
- // email is typically already indexed by Mongoose schema, but iamId may not be
97
- await db.collection('users').createIndex({ iamId: 1 }, { sparse: true });
98
-
99
- this.logger.debug('Performance indices ensured on session and users collections');
91
+ // All indices are idempotent (no-op if already exists) and independent run in parallel
92
+ await Promise.all([
93
+ // Session: token lookup (getSessionByToken) and user+expiry lookup (getActiveSessionForUser)
94
+ db.collection('session').createIndex({ token: 1 }),
95
+ db.collection('session').createIndex({ userId: 1, expiresAt: 1 }),
96
+ // Users: iamId lookup (mapSessionUser uses $or with email and iamId)
97
+ db.collection('users').createIndex({ iamId: 1 }, { sparse: true }),
98
+ // Account: userId lookup ($lookup in getMigrationStatus) and providerId filtering
99
+ db.collection('account').createIndex({ userId: 1 }),
100
+ db.collection('account').createIndex({ providerId: 1, userId: 1 }),
101
+ ]);
102
+
103
+ this.logger.debug('Performance indices ensured on session, users, and account collections');
100
104
  } catch (error) {
101
105
  // Non-fatal: indices improve performance but are not required for correctness
102
106
  this.logger.warn(`Could not create performance indices: ${error instanceof Error ? error.message : 'unknown'}`);
@@ -141,16 +141,15 @@ NestJS @Global() modules use "first wins" for provider registration. Without thi
141
141
 
142
142
  **Update:** `src/server/server.module.ts`
143
143
 
144
+ Use the `overrides` parameter of `CoreModule.forRoot()` (recommended since v11.22.0):
145
+
144
146
  ```typescript
145
- import { ErrorCodeModule as CoreErrorCodeModule } from '@lenne.tech/nest-server';
146
147
  import { ErrorCodeService } from './modules/error-code/error-code.service';
147
148
 
148
149
  @Module({
149
150
  imports: [
150
- CoreModule.forRoot(...),
151
- // Register with custom service
152
- CoreErrorCodeModule.forRoot({
153
- service: ErrorCodeService,
151
+ CoreModule.forRoot(envConfig, {
152
+ errorCode: { service: ErrorCodeService },
154
153
  }),
155
154
  // ... other modules
156
155
  ],
@@ -158,6 +157,22 @@ import { ErrorCodeService } from './modules/error-code/error-code.service';
158
157
  export class ServerModule {}
159
158
  ```
160
159
 
160
+ **Alternative** (for complex setups): disable auto-registration and import separately:
161
+
162
+ ```typescript
163
+ import { ErrorCodeModule as CoreErrorCodeModule } from '@lenne.tech/nest-server';
164
+ import { ErrorCodeService } from './modules/error-code/error-code.service';
165
+
166
+ @Module({
167
+ imports: [
168
+ CoreModule.forRoot({ ...envConfig, errorCode: { autoRegister: false } }),
169
+ CoreErrorCodeModule.forRoot({ service: ErrorCodeService }),
170
+ // ... other modules
171
+ ],
172
+ })
173
+ export class ServerModule {}
174
+ ```
175
+
161
176
  ---
162
177
 
163
178
  ## Scenario C: Custom Controller (For Custom Routes)
@@ -168,7 +183,7 @@ Use this when you need:
168
183
  - Different route paths
169
184
  - Additional REST endpoints
170
185
 
171
- **No custom module needed!** Use Core `ErrorCodeModule.forRoot()` with your custom controller and service.
186
+ **No custom module needed!** Use the `overrides` parameter of `CoreModule.forRoot()`.
172
187
 
173
188
  ### 1. Create Files
174
189
 
@@ -183,13 +198,29 @@ Files needed:
183
198
 
184
199
  **No `error-code.module.ts` needed!**
185
200
 
186
- ### 2. Disable Auto-Registration
201
+ ### 2. Register via CoreModule.forRoot() Overrides (Recommended)
187
202
 
188
- Same as Scenario B, Step 3.
203
+ **Update:** `src/server/server.module.ts`
189
204
 
190
- ### 3. Register via Core ErrorCodeModule
205
+ ```typescript
206
+ import { ErrorCodeController } from './modules/error-code/error-code.controller';
207
+ import { ErrorCodeService } from './modules/error-code/error-code.service';
191
208
 
192
- **Update:** `src/server/server.module.ts`
209
+ @Module({
210
+ imports: [
211
+ CoreModule.forRoot(envConfig, {
212
+ errorCode: {
213
+ controller: ErrorCodeController,
214
+ service: ErrorCodeService,
215
+ },
216
+ }),
217
+ // ... other modules
218
+ ],
219
+ })
220
+ export class ServerModule {}
221
+ ```
222
+
223
+ **Alternative** (for complex setups): disable auto-registration and import separately:
193
224
 
194
225
  ```typescript
195
226
  import { ErrorCodeModule } from '@lenne.tech/nest-server';
@@ -198,8 +229,7 @@ import { ErrorCodeService } from './modules/error-code/error-code.service';
198
229
 
199
230
  @Module({
200
231
  imports: [
201
- CoreModule.forRoot(...),
202
- // Use Core ErrorCodeModule with custom service and controller
232
+ CoreModule.forRoot({ ...envConfig, errorCode: { autoRegister: false } }),
203
233
  ErrorCodeModule.forRoot({
204
234
  controller: ErrorCodeController,
205
235
  service: ErrorCodeService,
@@ -12,8 +12,10 @@ import { IErrorCodeModuleConfig } from './interfaces/error-code.interfaces';
12
12
  *
13
13
  * @example
14
14
  * ```typescript
15
- * // Basic usage (auto-register in CoreModule)
16
- * // No explicit import needed - included in CoreModule
15
+ * // Basic usage (auto-register in CoreModule via overrides)
16
+ * CoreModule.forRoot(envConfig, {
17
+ * errorCode: { controller: ErrorCodeController, service: ErrorCodeService },
18
+ * })
17
19
  *
18
20
  * // Extended usage (with custom error registry - RECOMMENDED)
19
21
  * const ProjectErrors = {
@@ -25,13 +27,6 @@ import { IErrorCodeModuleConfig } from './interfaces/error-code.interfaces';
25
27
  * } as const satisfies IErrorRegistry;
26
28
  *
27
29
  * ErrorCodeModule.forRoot({ additionalErrorRegistry: ProjectErrors })
28
- *
29
- * // Extended usage (with custom controller and service)
30
- * ErrorCodeModule.forRoot({
31
- * additionalErrorRegistry: ProjectErrors,
32
- * controller: ErrorCodeController,
33
- * service: ErrorCodeService,
34
- * })
35
30
  * ```
36
31
  */
37
32
  @Global()
@@ -121,7 +121,18 @@ Normal (non-hierarchy) roles also work:
121
121
  async viewAuditLog(...) { ... }
122
122
  ```
123
123
 
124
- ### 8. Skip Tenant Check for Non-Tenant Endpoints
124
+ ### 8. System Roles as OR Alternatives
125
+
126
+ System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives **before** real role checks in `CoreTenantGuard`. If a system role grants access, real roles in the same `@Roles()` are alternatives, not requirements.
127
+
128
+ When `X-Tenant-Id` header is present and a system role grants access:
129
+
130
+ - `S_EVERYONE`: public — membership is optionally enriched but never blocks
131
+ - `S_USER`/`S_VERIFIED`: membership is validated (403 if not a member; admin bypass applies)
132
+
133
+ `@SkipTenantCheck()` suppresses membership validation for `S_USER`/`S_VERIFIED` paths — authentication/verification is still enforced, but no membership check runs even with a header present.
134
+
135
+ ### 9. Skip Tenant Check for Non-Tenant Endpoints
125
136
 
126
137
  Use `@SkipTenantCheck()` for endpoints that intentionally work without tenant context:
127
138
 
@@ -133,7 +144,7 @@ import { SkipTenantCheck, Roles, RoleEnum } from '@lenne.tech/nest-server';
133
144
  async listMyTenants() { ... }
134
145
  ```
135
146
 
136
- ### 9. BetterAuth (IAM) Coexistence
147
+ ### 10. BetterAuth (IAM) Coexistence
137
148
 
138
149
  When both `multiTenancy` and `betterAuth` are active, IAM endpoints (sign-in, sign-up, session, etc.)
139
150
  automatically skip tenant validation when no `X-Tenant-Id` header is sent (`betterAuth.skipTenantCheck: true`, default).
@@ -138,9 +138,33 @@ Roles not in `roleHierarchy` use exact match — no higher role can compensate:
138
138
  async auditLog(@CurrentTenant() tenantId: string) { ... }
139
139
  ```
140
140
 
141
+ ### System Roles as OR Alternatives
142
+
143
+ When `@Roles()` includes system roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`), `CoreTenantGuard` checks them **before** real roles in priority order. If a system role is satisfied, access is granted immediately — real roles in the same decorator are OR alternatives, not additional requirements.
144
+
145
+ ```typescript
146
+ @Roles(RoleEnum.ADMIN) // Class: admin can always access all methods
147
+ @Controller('users')
148
+ class UserController {
149
+ @Roles(RoleEnum.S_USER) // Method: any authenticated user (extends class ADMIN via OR)
150
+ getProfile() { ... } // → admin OR authenticated user can access
151
+
152
+ @Roles(RoleEnum.S_EVERYONE) // Method: public endpoint (extends class ADMIN via OR)
153
+ getPublicInfo() { ... } // → anyone can access
154
+ }
155
+ ```
156
+
157
+ **Method-level system roles take precedence** for the system role check. Class `@Roles(S_EVERYONE)` does not make a method `@Roles(S_USER)` endpoint public — the method's `S_USER` applies.
158
+
159
+ | System Role | No Header | Header Present |
160
+ | ----------- | --------------------- | -------------------------------------------------- |
161
+ | S_EVERYONE | Pass (public) | Pass + optional tenant context enrichment |
162
+ | S_USER | Pass if authenticated | Pass if authenticated member; admin bypass applies |
163
+ | S_VERIFIED | Pass if verified | Pass if verified member; admin bypass applies |
164
+
141
165
  ### Tenant Context Rule
142
166
 
143
- **When a tenant header is present:** Only `membership.role` is checked. `user.roles` is ignored (except ADMIN bypass).
167
+ **When a tenant header is present:** For real role checks, only `membership.role` is checked. `user.roles` is ignored (except ADMIN bypass). For system role checks (`S_USER`, `S_VERIFIED`), membership is validated to set tenant context (`tenantId`, `tenantRole`), but access depends on membership existence, not role level.
144
168
 
145
169
  **When no tenant header:** `user.roles` is checked instead. Hierarchy roles use level comparison, normal roles use exact match.
146
170
 
@@ -161,6 +185,7 @@ async listMyTenants() { ... }
161
185
  ```
162
186
 
163
187
  Note: `@SkipTenantCheck()` with hierarchy roles still checks `user.roles` (no tenant context).
188
+ It also suppresses tenant membership validation for `S_USER`/`S_VERIFIED` system role paths — when set, these roles still enforce authentication/verification but skip the membership check even when a tenant header is present.
164
189
 
165
190
  ### Admin Bypass
166
191