Skip to content

Commit b0dee82

Browse files
kaihaaseclaude
andcommitted
docs(better-auth): extract architecture docs to separate file
Move "Architecture: Why Custom Controllers?" section from README.md to dedicated ARCHITECTURE.md file for better documentation structure. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 64a5ddb commit b0dee82

File tree

4 files changed

+267
-5
lines changed

4 files changed

+267
-5
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Architecture: Why Custom Controllers?
2+
3+
The `CoreBetterAuthController` implements custom endpoints instead of directly using native Better-Auth endpoints. This is **necessary** for the nest-server hybrid auth system.
4+
5+
## 1. Hybrid-Auth-System (Legacy + Better-Auth)
6+
7+
The nest-server supports bidirectional authentication:
8+
- **Legacy Auth → Better-Auth**: Users created via Legacy Auth can sign in via Better-Auth
9+
- **Better-Auth → Legacy Auth**: Users created via Better-Auth can sign in via Legacy Auth
10+
11+
This requires custom logic that cannot be implemented via Better-Auth hooks alone.
12+
13+
## 2. Why Not Better-Auth Hooks?
14+
15+
Better-Auth hooks have fundamental limitations that prevent full implementation of our requirements:
16+
17+
| Requirement | Hook Support | Reason |
18+
|-------------|--------------|--------|
19+
| Legacy user migration | ⚠️ Partial | Requires global DB access outside NestJS DI |
20+
| Password sync to Legacy | ❌ No | **After-hooks don't have access to plaintext password** |
21+
| Custom response format | ❌ No | **Hooks cannot modify HTTP response** |
22+
| Multi-cookie setting | ❌ No | **Hooks cannot set cookies** |
23+
| User mapping with roles | ❌ No | Requires NestJS Dependency Injection |
24+
| Session token injection | ❌ No | Before-hooks cannot inject tokens into requests |
25+
26+
## 3. Hook Limitations Explained
27+
28+
### After-Hooks Cannot Change Response
29+
30+
```typescript
31+
// ❌ This does NOT work - return value is ignored
32+
hooks: {
33+
after: createAuthMiddleware(async (ctx) => {
34+
ctx.response.body.customField = 'value'; // Ignored!
35+
return { response: modifiedResponse }; // Also ignored!
36+
}),
37+
}
38+
```
39+
40+
### After-Hooks Don't Have Plaintext Password
41+
42+
```typescript
43+
// ❌ Cannot sync password because it's already hashed
44+
hooks: {
45+
after: [
46+
{
47+
matcher: (ctx) => ctx.path === '/sign-up/email',
48+
handler: async (ctx) => {
49+
// ctx.body.password is ALREADY HASHED at this point
50+
// We cannot call syncPasswordToLegacy() without plaintext!
51+
},
52+
},
53+
],
54+
}
55+
```
56+
57+
### Hooks Don't Have NestJS DI Access
58+
59+
```typescript
60+
// ❌ Hooks are configured in betterAuth(), not in NestJS context
61+
export const auth = betterAuth({
62+
hooks: {
63+
// No access to NestJS services here!
64+
// this.userService, this.emailService, etc. are unavailable
65+
},
66+
});
67+
```
68+
69+
## 4. What Custom Endpoints Do
70+
71+
| Endpoint | Custom Logic | Why Required |
72+
|----------|--------------|--------------|
73+
| `/sign-in/email` | Legacy migration, PW normalization, 2FA handling | Migration needs plaintext password |
74+
| `/sign-up/email` | PW normalization, Legacy sync, User linking | Sync needs plaintext password |
75+
| `/sign-out` | Multi-cookie clearing | Response modification |
76+
| `/session` | User mapping with roles | NestJS service access |
77+
| Plugin routes | Session token injection | Request modification |
78+
79+
## 5. Native Handler Where Possible
80+
81+
Despite custom endpoints, we use Better-Auth's native handler where appropriate:
82+
- **Plugin routes** (Passkey, 2FA, OAuth) → `authInstance.handler()`
83+
- **2FA verification flow** → Native handler for correct cookie setting
84+
- **Passkey authentication** → Native WebAuthn handling
85+
86+
## 6. Alternative Approaches Considered
87+
88+
| Approach | Evaluation |
89+
|----------|------------|
90+
| **Full Hook Approach** | ❌ Not feasible - missing plaintext password, no response modification |
91+
| **Hybrid with Global DB** | ⚠️ Possible but anti-pattern - bypasses NestJS DI, harder to test |
92+
| **Custom Controller (current)** | ✅ Best balance - NestJS DI access, testable, maintainable |
93+
94+
## Conclusion
95+
96+
The custom controller architecture is **necessary complexity**, not unnecessary overhead. It enables:
97+
- ✅ Legacy Auth compatibility
98+
- ✅ Bidirectional password synchronization
99+
- ✅ Multi-cookie support
100+
- ✅ Custom user mapping with roles
101+
- ✅ Proper 2FA cookie handling
102+
- ✅ Full NestJS Dependency Injection access

src/core/modules/better-auth/INTEGRATION-CHECKLIST.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,84 @@ async function deletePasskey(passkeyId: string) {
439439
440440
---
441441
442+
## Better-Auth Hooks: Limitations & Warnings
443+
444+
### Why nest-server Uses Custom Controllers
445+
446+
nest-server implements custom REST endpoints instead of relying solely on Better-Auth hooks. This is **by design** due to fundamental hook limitations.
447+
448+
### Hook Limitations Summary
449+
450+
| Limitation | Impact |
451+
|------------|--------|
452+
| **After-hooks cannot access plaintext password** | Cannot sync password to Legacy Auth after sign-up |
453+
| **Hooks cannot modify HTTP response** | Cannot customize response format or add custom fields |
454+
| **Hooks cannot set cookies** | Cannot implement multi-cookie auth strategy |
455+
| **No NestJS Dependency Injection** | Cannot access services like UserService, EmailService |
456+
| **Before-hooks cannot inject tokens** | Cannot add session tokens to request headers |
457+
458+
### What You CAN Do with Hooks
459+
460+
Better-Auth hooks are suitable for:
461+
- ✅ Logging and analytics (side effects only)
462+
- ✅ Sending notifications after events
463+
- ✅ Simple validation in before-hooks
464+
- ✅ Database writes using global connection (not recommended)
465+
466+
### What You CANNOT Do with Hooks
467+
468+
Do NOT try to implement these via hooks:
469+
- ❌ Password synchronization between auth systems
470+
- ❌ Custom response formats
471+
- ❌ Setting authentication cookies
472+
- ❌ User role mapping
473+
- ❌ Legacy auth migration
474+
475+
### Recommended Approach
476+
477+
If you need custom authentication logic:
478+
479+
1. **Extend the Controller** - Override methods in `BetterAuthController`
480+
2. **Use NestJS Services** - Inject services via constructor
481+
3. **Call super()** - Reuse base implementation where possible
482+
483+
```typescript
484+
// Correct: Custom logic via controller extension
485+
@Controller('iam')
486+
export class BetterAuthController extends CoreBetterAuthController {
487+
constructor(
488+
betterAuthService: CoreBetterAuthService,
489+
userMapper: CoreBetterAuthUserMapper,
490+
configService: ConfigService,
491+
private readonly analyticsService: AnalyticsService, // Custom service
492+
) {
493+
super(betterAuthService, userMapper, configService);
494+
}
495+
496+
@Post('sign-up/email')
497+
@Roles(RoleEnum.S_EVERYONE)
498+
override async signUp(
499+
@Res({ passthrough: true }) res: Response,
500+
@Body() input: CoreBetterAuthSignUpInput,
501+
): Promise<CoreBetterAuthResponse> {
502+
const result = await super.signUp(res, input);
503+
504+
// Custom logic with full NestJS DI access
505+
if (result.success) {
506+
await this.analyticsService.trackSignUp(result.user.id);
507+
}
508+
509+
return result;
510+
}
511+
}
512+
```
513+
514+
### Further Reading
515+
516+
See README.md section "Architecture: Why Custom Controllers?" for detailed explanation.
517+
518+
---
519+
442520
## Detailed Documentation
443521
444522
For complete configuration options, API reference, and advanced topics:

src/core/modules/better-auth/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,6 +1570,12 @@ describe('My Tests', () => {
15701570
});
15711571
```
15721572

1573+
## Architecture: Why Custom Controllers?
1574+
1575+
See **[ARCHITECTURE.md](./ARCHITECTURE.md)** for detailed documentation on why custom controllers are necessary for the hybrid auth system.
1576+
1577+
---
1578+
15731579
## Troubleshooting
15741580

15751581
### Better-Auth endpoints return 404

src/core/modules/better-auth/core-better-auth.controller.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,32 @@ export class CoreBetterAuthSignUpInput {
126126
* This controller follows the same pattern as CoreAuthController and can be
127127
* extended by project-specific implementations.
128128
*
129+
* ## Why Custom Controller Instead of Native Better-Auth Endpoints?
130+
*
131+
* This controller implements custom endpoints rather than directly using Better-Auth's
132+
* native API. This architecture is **necessary** for nest-server's requirements:
133+
*
134+
* ### 1. Better-Auth Hooks Cannot:
135+
* - Access plaintext passwords in after-hooks (needed for Legacy sync)
136+
* - Modify HTTP responses (needed for custom response format)
137+
* - Set cookies (needed for multi-cookie auth strategy)
138+
* - Access NestJS Dependency Injection (needed for UserService, etc.)
139+
*
140+
* ### 2. Custom Endpoints Enable:
141+
* - **Hybrid Auth**: Bidirectional Legacy Auth ↔ Better-Auth synchronization
142+
* - **Password Normalization**: SHA256 pre-hashing for security
143+
* - **Legacy Migration**: Automatic migration of legacy users on sign-in
144+
* - **Multi-Cookie Support**: Setting multiple auth cookies for compatibility
145+
* - **Role Mapping**: Integration with nest-server's role-based access control
146+
*
147+
* ### 3. Native Handler Where Possible:
148+
* Despite custom endpoints, we use `authInstance.handler()` for:
149+
* - Plugin routes (Passkey, 2FA, OAuth)
150+
* - 2FA verification (for correct cookie handling)
151+
* - All plugin-provided functionality
152+
*
153+
* See README.md section "Architecture: Why Custom Controllers?" for details.
154+
*
129155
* @example
130156
* ```typescript
131157
* // In your project - src/server/modules/better-auth/better-auth.controller.ts
@@ -169,14 +195,22 @@ export class CoreBetterAuthController {
169195
/**
170196
* Sign in with email and password
171197
*
172-
* This endpoint handles legacy user migration and password normalization.
198+
* **Why Custom Implementation (not hooks):**
199+
* - Hooks cannot access plaintext password for legacy migration
200+
* - Hooks cannot modify response format
201+
* - Hooks cannot set multi-cookie auth strategy
173202
*
174-
* Flow:
203+
* **Flow:**
175204
* 1. Try legacy user migration if the user exists in legacy system
176-
* 2. Normalize password to SHA256 format
205+
* → Requires plaintext password (unavailable in after-hooks)
206+
* 2. Normalize password to SHA256 format for Better Auth
177207
* 3. Call Better Auth API directly for consistent response format
178208
* 4. For 2FA: Use native handler to ensure cookies are set correctly
179-
* 5. Return response with session cookies
209+
* → Hooks cannot set cookies, so we use authInstance.handler()
210+
* 5. Return response with multiple auth cookies
211+
* → Hooks cannot modify response or set cookies
212+
*
213+
* @see README.md "Architecture: Why Custom Controllers?"
180214
*/
181215
@ApiBody({ type: CoreBetterAuthSignInInput })
182216
@ApiCreatedResponse({ description: 'Signed in successfully', type: CoreBetterAuthResponse })
@@ -317,6 +351,23 @@ export class CoreBetterAuthController {
317351

318352
/**
319353
* Sign up with email and password
354+
*
355+
* **Why Custom Implementation (not hooks):**
356+
* - After-hooks don't have access to plaintext password
357+
* → Cannot call syncPasswordToLegacy() in hooks
358+
* - Hooks cannot access NestJS services
359+
* → Cannot use UserMapper for user linking
360+
* - Hooks cannot modify response format
361+
*
362+
* **Custom Logic:**
363+
* 1. Normalize password to SHA256 for Better Auth storage
364+
* 2. Create user via Better Auth API
365+
* 3. Link user to Legacy system (requires NestJS UserMapper)
366+
* 4. Sync plaintext password to Legacy Auth (bcrypt hash)
367+
* → CRITICAL: This requires plaintext, unavailable in after-hooks
368+
* 5. Return response with session cookies
369+
*
370+
* @see README.md "Architecture: Why Custom Controllers?"
320371
*/
321372
@ApiBody({ type: CoreBetterAuthSignUpInput })
322373
@ApiCreatedResponse({ description: 'Signed up successfully', type: CoreBetterAuthResponse })
@@ -384,7 +435,13 @@ export class CoreBetterAuthController {
384435
/**
385436
* Sign out (logout)
386437
*
438+
* **Why Custom Implementation (not hooks):**
439+
* - Must clear multiple cookies (token, session, better-auth.session_token, etc.)
440+
* - Hooks cannot modify response or set/clear cookies
441+
*
387442
* NOTE: Better-Auth uses POST for sign-out (matches better-auth convention)
443+
*
444+
* @see README.md "Architecture: Why Custom Controllers?"
388445
*/
389446
@ApiOkResponse({ description: 'Signed out successfully', type: CoreBetterAuthResponse })
390447
@ApiOperation({ description: 'Sign out from Better-Auth', summary: 'Sign Out' })
@@ -417,6 +474,13 @@ export class CoreBetterAuthController {
417474

418475
/**
419476
* Get current session
477+
*
478+
* **Why Custom Implementation (not hooks):**
479+
* - Must map Better Auth user to nest-server user with roles
480+
* - Hooks cannot access NestJS UserMapper service
481+
* - Custom response format with mapped user data
482+
*
483+
* @see README.md "Architecture: Why Custom Controllers?"
420484
*/
421485
@ApiOkResponse({ description: 'Current session', type: CoreBetterAuthResponse })
422486
@ApiOperation({ description: 'Get current session from Better-Auth', summary: 'Get Session' })
@@ -454,7 +518,17 @@ export class CoreBetterAuthController {
454518
/**
455519
* Catch-all route for all other Better Auth plugin endpoints.
456520
*
457-
* This route handles:
521+
* **This route USES the native Better Auth handler** via `authInstance.handler()`.
522+
* It's the best of both worlds:
523+
* - Custom endpoints where we need NestJS features (sign-in, sign-up, etc.)
524+
* - Native handler for plugins that work correctly out-of-the-box
525+
*
526+
* **Why Not Fully Native:**
527+
* Even this catch-all requires custom logic:
528+
* - Session token injection into request (before-hooks can't inject tokens)
529+
* - Converting Express Request to Web Standard Request
530+
*
531+
* **Handles:**
458532
* - Passkey/WebAuthn (all endpoints)
459533
* - Two-Factor Authentication (all endpoints)
460534
* - Social Login OAuth flows
@@ -467,6 +541,8 @@ export class CoreBetterAuthController {
467541
*
468542
* Better Auth handles authentication internally - it returns appropriate
469543
* errors (401, 403) if a user is not authenticated for protected endpoints.
544+
*
545+
* @see README.md "Architecture: Why Custom Controllers?"
470546
*/
471547
@All('*path')
472548
@Roles(RoleEnum.S_EVERYONE)

0 commit comments

Comments
 (0)