@quanticjs/create-app 0.1.1

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 (136) hide show
  1. package/dist/deps.d.ts +4 -0
  2. package/dist/deps.js +97 -0
  3. package/dist/deps.js.map +1 -0
  4. package/dist/generators/backend.d.ts +2 -0
  5. package/dist/generators/backend.js +20 -0
  6. package/dist/generators/backend.js.map +1 -0
  7. package/dist/generators/bff.d.ts +2 -0
  8. package/dist/generators/bff.js +9 -0
  9. package/dist/generators/bff.js.map +1 -0
  10. package/dist/generators/claude.d.ts +2 -0
  11. package/dist/generators/claude.js +45 -0
  12. package/dist/generators/claude.js.map +1 -0
  13. package/dist/generators/docker.d.ts +2 -0
  14. package/dist/generators/docker.js +10 -0
  15. package/dist/generators/docker.js.map +1 -0
  16. package/dist/generators/e2e.d.ts +2 -0
  17. package/dist/generators/e2e.js +8 -0
  18. package/dist/generators/e2e.js.map +1 -0
  19. package/dist/generators/frontend.d.ts +2 -0
  20. package/dist/generators/frontend.js +28 -0
  21. package/dist/generators/frontend.js.map +1 -0
  22. package/dist/generators/module.d.ts +2 -0
  23. package/dist/generators/module.js +35 -0
  24. package/dist/generators/module.js.map +1 -0
  25. package/dist/generators/root.d.ts +2 -0
  26. package/dist/generators/root.js +7 -0
  27. package/dist/generators/root.js.map +1 -0
  28. package/dist/generators/scripts.d.ts +2 -0
  29. package/dist/generators/scripts.js +10 -0
  30. package/dist/generators/scripts.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +40 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/prompts.d.ts +8 -0
  35. package/dist/prompts.js +53 -0
  36. package/dist/prompts.js.map +1 -0
  37. package/dist/scaffold.d.ts +2 -0
  38. package/dist/scaffold.js +79 -0
  39. package/dist/scaffold.js.map +1 -0
  40. package/dist/utils/exec.d.ts +2 -0
  41. package/dist/utils/exec.js +14 -0
  42. package/dist/utils/exec.js.map +1 -0
  43. package/dist/utils/template.d.ts +10 -0
  44. package/dist/utils/template.js +20 -0
  45. package/dist/utils/template.js.map +1 -0
  46. package/dist/utils/validate.d.ts +4 -0
  47. package/dist/utils/validate.js +27 -0
  48. package/dist/utils/validate.js.map +1 -0
  49. package/package.json +50 -0
  50. package/templates/backend/app.module.ts.ejs +61 -0
  51. package/templates/backend/bff.controller.ts.ejs +60 -0
  52. package/templates/backend/bff.module.ts.ejs +10 -0
  53. package/templates/backend/bff.service.ts.ejs +6 -0
  54. package/templates/backend/create-schema.migration.ts.ejs +15 -0
  55. package/templates/backend/data-source.ts.ejs +13 -0
  56. package/templates/backend/env.example.ejs +30 -0
  57. package/templates/backend/main.ts.ejs +43 -0
  58. package/templates/backend/module.ts.ejs +14 -0
  59. package/templates/backend/nest-cli.json.ejs +8 -0
  60. package/templates/backend/package.json.ejs +23 -0
  61. package/templates/backend/tsconfig.build.json.ejs +4 -0
  62. package/templates/backend/tsconfig.json.ejs +24 -0
  63. package/templates/claude/CLAUDE.md.ejs +86 -0
  64. package/templates/claude/hooks/auto-format.sh +22 -0
  65. package/templates/claude/hooks/check-secrets.sh +49 -0
  66. package/templates/claude/hooks/guard-destructive.sh +42 -0
  67. package/templates/claude/hooks/on-compaction.sh +29 -0
  68. package/templates/claude/mcp.json +10 -0
  69. package/templates/claude/rules/api-patterns.md +86 -0
  70. package/templates/claude/rules/auth-patterns.md +109 -0
  71. package/templates/claude/rules/backend-patterns.md +421 -0
  72. package/templates/claude/rules/database-patterns.md +96 -0
  73. package/templates/claude/rules/docker-patterns.md +86 -0
  74. package/templates/claude/rules/frontend-patterns.md +262 -0
  75. package/templates/claude/rules/observability-backend.md +132 -0
  76. package/templates/claude/rules/observability-frontend.md +49 -0
  77. package/templates/claude/rules/playwright-mcp.md +80 -0
  78. package/templates/claude/rules/resilience-ops.md +103 -0
  79. package/templates/claude/rules/testing-e2e-ui.md +190 -0
  80. package/templates/claude/rules/testing-patterns.md +94 -0
  81. package/templates/claude/rules/workflow-backend.md +64 -0
  82. package/templates/claude/rules/workflow-frontend.md +60 -0
  83. package/templates/claude/settings.json +68 -0
  84. package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
  85. package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
  86. package/templates/claude/skills/add-entity/SKILL.md +56 -0
  87. package/templates/claude/skills/add-event/SKILL.md +127 -0
  88. package/templates/claude/skills/add-feature/SKILL.md +20 -0
  89. package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
  90. package/templates/claude/skills/add-handler/SKILL.md +105 -0
  91. package/templates/claude/skills/add-integration/SKILL.md +176 -0
  92. package/templates/claude/skills/add-migration/SKILL.md +20 -0
  93. package/templates/claude/skills/add-module/SKILL.md +89 -0
  94. package/templates/claude/skills/add-realtime/SKILL.md +119 -0
  95. package/templates/claude/skills/audit-rules/SKILL.md +120 -0
  96. package/templates/claude/skills/debugging/SKILL.md +105 -0
  97. package/templates/claude/skills/docker-dev/SKILL.md +86 -0
  98. package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
  99. package/templates/claude/skills/e2e-full/SKILL.md +132 -0
  100. package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
  101. package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
  102. package/templates/claude/skills/fix-bug/SKILL.md +33 -0
  103. package/templates/claude/skills/implement-spec/SKILL.md +98 -0
  104. package/templates/claude/skills/review-code/SKILL.md +109 -0
  105. package/templates/claude/skills/review-spec/SKILL.md +216 -0
  106. package/templates/claude/skills/run-tests/SKILL.md +37 -0
  107. package/templates/claude/skills/specify/SKILL.md +87 -0
  108. package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
  109. package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
  110. package/templates/docker/Dockerfile.client.ejs +14 -0
  111. package/templates/docker/Dockerfile.ejs +28 -0
  112. package/templates/docker/docker-compose.test.yml.ejs +54 -0
  113. package/templates/docker/docker-compose.yml.ejs +76 -0
  114. package/templates/docker/nginx.conf.ejs +21 -0
  115. package/templates/frontend/App.tsx.ejs +64 -0
  116. package/templates/frontend/DashboardPage.tsx.ejs +37 -0
  117. package/templates/frontend/LoginPage.tsx.ejs +20 -0
  118. package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
  119. package/templates/frontend/api-client.ts.ejs +15 -0
  120. package/templates/frontend/index.css.ejs +57 -0
  121. package/templates/frontend/index.html.ejs +13 -0
  122. package/templates/frontend/main.tsx.ejs +10 -0
  123. package/templates/frontend/package.json.ejs +16 -0
  124. package/templates/frontend/playwright.config.ts.ejs +20 -0
  125. package/templates/frontend/postcss.config.js.ejs +3 -0
  126. package/templates/frontend/smoke.spec.ts.ejs +37 -0
  127. package/templates/frontend/tailwind.config.ts.ejs +56 -0
  128. package/templates/frontend/tsconfig.json.ejs +25 -0
  129. package/templates/frontend/tsconfig.node.json.ejs +15 -0
  130. package/templates/frontend/utils.ts.ejs +6 -0
  131. package/templates/frontend/vite-env.d.ts.ejs +1 -0
  132. package/templates/frontend/vite.config.ts.ejs +20 -0
  133. package/templates/root/gitignore.ejs +9 -0
  134. package/templates/root/prettierrc.ejs +7 -0
  135. package/templates/scripts/init-db.sh.ejs +8 -0
  136. package/templates/scripts/save-auth-state.ts.ejs +24 -0
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+ # Notification hook: fires after context compaction.
3
+ # Re-injects key context so Claude doesn't lose track of current work.
4
+ set -uo pipefail
5
+
6
+ echo "=== POST-COMPACTION CONTEXT ==="
7
+
8
+ BRANCH=$(git branch --show-current 2>/dev/null)
9
+ if [[ -n "$BRANCH" ]]; then
10
+ echo "Branch: $BRANCH"
11
+ fi
12
+
13
+ COMMITS=$(git log --oneline -10 main..HEAD 2>/dev/null)
14
+ if [[ -n "$COMMITS" ]]; then
15
+ echo ""
16
+ echo "Commits on this branch:"
17
+ echo "$COMMITS"
18
+ fi
19
+
20
+ CHANGES=$(git status --short 2>/dev/null | head -20)
21
+ if [[ -n "$CHANGES" ]]; then
22
+ echo ""
23
+ echo "Uncommitted changes:"
24
+ echo "$CHANGES"
25
+ fi
26
+
27
+ echo ""
28
+ echo "Re-read CLAUDE.md and .claude/rules/ for conventions."
29
+ echo "=== END ==="
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "playwright": {
4
+ "command": "npx",
5
+ "args": [
6
+ "@playwright/mcp@0.0.75"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,86 @@
1
+ ---
2
+ globs: "src/**/*.ts"
3
+ ---
4
+
5
+ # API Documentation Patterns
6
+
7
+ ## OpenAPI 3.1 — Code-First with @nestjs/swagger
8
+
9
+ | URL | Purpose |
10
+ |-----|---------|
11
+ | `/api/docs` | Swagger UI (interactive) |
12
+ | `/api/docs-json` | OpenAPI 3.1 JSON spec |
13
+
14
+ ## Controller Decorators (MANDATORY)
15
+
16
+ Every endpoint annotated with `@ApiOperation`, `@ApiResponse`, `@ApiBody`, `@ApiTags`.
17
+
18
+ ```typescript
19
+ @ApiTags('items')
20
+ @Controller('items')
21
+ export class ItemsController {
22
+ @Post()
23
+ @ApiOperation({ summary: 'Create a new item' })
24
+ @ApiResponse({ status: 201, type: ItemResponseDto })
25
+ @ApiResponse({ status: 400, type: ErrorResponseDto })
26
+ create(@Body() dto: CreateItemDto) { ... }
27
+ }
28
+ ```
29
+
30
+ ## DTO Decorators
31
+
32
+ DTOs use both `class-validator` (runtime) and `@ApiProperty` (docs):
33
+
34
+ ```typescript
35
+ export class CreateItemDto {
36
+ @ApiProperty({ description: 'Item name', minLength: 1, maxLength: 200 })
37
+ @IsString()
38
+ @MinLength(1)
39
+ @MaxLength(200)
40
+ title: string;
41
+ }
42
+ ```
43
+
44
+ ## Response DTOs
45
+
46
+ All API responses use typed response DTOs — never raw entity objects.
47
+
48
+ ## Result<T> → HTTP Mapping (RFC 9457 Problem Details)
49
+
50
+ Error responses use `Content-Type: application/problem+json` with RFC 9457 problem-details shape:
51
+
52
+ ```json
53
+ {
54
+ "type": "https://arex.dev/errors/NOT_FOUND",
55
+ "title": "Not Found",
56
+ "status": 404,
57
+ "detail": "Item not found",
58
+ "instance": "/api/items/123",
59
+ "correlationId": "abc-123"
60
+ }
61
+ ```
62
+
63
+ | Result | HTTP |
64
+ |--------|------|
65
+ | `Result.success(value)` | 200/201 |
66
+ | `ErrorType.ValidationError` | 400 |
67
+ | `ErrorType.Unauthorized` | 401 |
68
+ | `ErrorType.Forbidden` | 403 |
69
+ | `ErrorType.NotFound` | 404 |
70
+ | `ErrorType.Conflict` | 409 |
71
+ | `ErrorType.UnprocessableEntity` | 422 |
72
+ | `ErrorType.InternalError` | 500 |
73
+
74
+ ## Environment Availability
75
+
76
+ | Env | Swagger UI | JSON Spec |
77
+ |-----|-----------|-----------|
78
+ | Local / Dev / Staging | Enabled | Enabled |
79
+ | **Production** | **Disabled** | **Disabled** |
80
+
81
+ ## NEVER
82
+
83
+ - **NEVER** maintain separate Markdown/Wiki API documentation
84
+ - **NEVER** return raw entities from controllers — use response DTOs
85
+ - **NEVER** leave endpoints undocumented
86
+ - **NEVER** enable Swagger UI in production
@@ -0,0 +1,109 @@
1
+ ---
2
+ globs: "src/bff/**/*.ts, client/src/**/*.{ts,tsx}"
3
+ ---
4
+
5
+ # Authentication & Authorization Patterns
6
+
7
+ ## BFF Authentication (Backend-for-Frontend)
8
+
9
+ All auth goes through the NestJS BFF module (`src/bff/`). Tokens stored server-side in Redis. Browser gets httpOnly cookies only.
10
+
11
+ ### BFF Endpoints
12
+
13
+ | Endpoint | Method | Purpose |
14
+ |----------|--------|---------|
15
+ | `/auth/login` | GET | Redirect to Keycloak with PKCE. Query params: `provider`, `returnTo` |
16
+ | `/auth/callback` | GET | OIDC callback — exchange code for tokens, set httpOnly cookie, redirect to SPA |
17
+ | `/auth/refresh` | POST | Refresh access token using refresh token from Redis |
18
+ | `/auth/logout` | POST | Clear cookies, revoke Keycloak session, invalidate Redis session |
19
+ | `/auth/me` | GET | Return current user info + role + permissions |
20
+
21
+ ### Cookie Configuration
22
+
23
+ ```typescript
24
+ {
25
+ httpOnly: true, // invisible to JS — immune to XSS
26
+ secure: true, // HTTPS only (except local dev)
27
+ sameSite: 'lax', // CSRF protection
28
+ path: '/',
29
+ maxAge: 7 * 24 * 3600 // 7 days
30
+ }
31
+ ```
32
+
33
+ ### Token Flow
34
+
35
+ 1. Browser sends request with httpOnly cookie (automatic — no JS involved)
36
+ 2. BFF middleware reads session ID from cookie, looks up access token in Redis
37
+ 3. If access token exists → inject `Authorization: Bearer` header
38
+ 4. If access token expired but refresh token exists → middleware calls Keycloak to get a new access token, stores it in Redis, injects it (transparent to frontend)
39
+ 5. If both expired → no header injected, `JwtAuthGuard` returns 401 (session dead, user must re-login)
40
+ 6. `JwtAuthGuard` validates the token as normal
41
+
42
+ - Access tokens: Redis, short-lived (configured in Keycloak)
43
+ - Refresh tokens: Redis, 7 days, rotating
44
+ - Session cookie: browser httpOnly, 7 days
45
+
46
+ ### Token Storage (Mobile — React Native)
47
+
48
+ - **Tier 1 (sensitive):** `expo-secure-store` or `react-native-keychain` — tokens, credentials
49
+ - **Tier 2 (preferences):** `AsyncStorage` — theme, locale, onboarding flags
50
+ - **Tier 3 (cached data):** TanStack Query persistence
51
+
52
+ ## Authorization — Backend is Source of Truth
53
+
54
+ **Permissions are decided on the backend. Always.** The frontend is a hint layer, not a security boundary.
55
+
56
+ ### Frontend Permission Expression
57
+
58
+ ```tsx
59
+ const { can } = usePermissions();
60
+ if (can('invoice.delete')) { /* ... */ }
61
+
62
+ <Can permission="invoice.delete">
63
+ <DeleteButton />
64
+ </Can>
65
+ ```
66
+
67
+ Permissions fetched from `/auth/me` on login, refreshed with session.
68
+
69
+ ### Frontend Auth State
70
+
71
+ ```typescript
72
+ const { data: session } = useQuery({
73
+ queryKey: ['auth', 'session'],
74
+ queryFn: () => api.get('/auth/me'),
75
+ retry: false,
76
+ });
77
+ const isAuthenticated = !!session;
78
+ ```
79
+
80
+ ### Login/Logout
81
+
82
+ ```typescript
83
+ // Login — redirect to Keycloak via BFF
84
+ window.location.href = '/auth/login?provider=google&returnTo=/dashboard';
85
+
86
+ // Logout
87
+ await fetch('/auth/logout', { method: 'POST', credentials: 'include' });
88
+ ```
89
+
90
+ ### Socket.IO
91
+
92
+ ```typescript
93
+ const socket = io({ withCredentials: true }); // cookie sent on handshake
94
+ ```
95
+
96
+ ## NEVER
97
+
98
+ - **NEVER** store tokens in `localStorage` or `sessionStorage` (violates RFC 9700)
99
+ - **NEVER** set `Authorization` headers from frontend code
100
+ - **NEVER** read token claims in frontend JS — call `/auth/me`
101
+ - **NEVER** pass tokens in URL query parameters
102
+ - **NEVER** return raw tokens to the frontend
103
+ - **NEVER** bypass the BFF for auth
104
+ - **NEVER** use `AsyncStorage` for tokens on mobile
105
+ - **NEVER** bundle secrets into the mobile app binary
106
+ - **NEVER** implement frontend-only permission checks without backend enforcement
107
+ - **NEVER** use `localStorage`/`sessionStorage` to mock auth in tests — mock `/auth/me` via TanStack Query or `page.route()`
108
+ - **NEVER** implement token refresh logic in the frontend — the BFF middleware handles it server-side (RFC 9700)
109
+ - **NEVER** add 401 retry/intercept logic in the frontend API client — a 401 means the session is dead, not that a token needs refreshing
@@ -0,0 +1,421 @@
1
+ ---
2
+ globs: "src/**/*.ts"
3
+ ---
4
+
5
+ # Backend Patterns
6
+
7
+ ## Modular Monolith
8
+
9
+ Two deployable artifacts: one NestJS backend image, one React frontend image. No microservices.
10
+
11
+ ### Module Structure
12
+
13
+ ```
14
+ src/
15
+ <module>/ # Domain module (e.g., identity, billing)
16
+ bff/ # BFF authentication
17
+ shared/ # Guards, filters, interceptors
18
+ ```
19
+
20
+ ### Module Boundary Rules
21
+
22
+ - Modules communicate through `CommandBus`/`QueryBus` — never import another module's services or repositories
23
+ - Each module owns its own PostgreSQL schema (e.g., `identity.*`, `billing.*`)
24
+ - Async inter-module communication uses Redis Streams
25
+ - Only commands, queries, and DTOs are exported from a module
26
+
27
+ ## POST-IMPLEMENTATION CHECKLIST (run after every command/handler pair)
28
+
29
+ Before committing any command + handler:
30
+ - [ ] Command class has `@Validate(XxxValidator)` decorator → grep the command file for `@Validate`
31
+ - [ ] `.validator.ts` file exists with Zod schema + `ICommandValidator<T>`
32
+ - [ ] Handler uses `getTransactionalRepo(this.xxxRepo)` — never `this.xxxRepo` directly
33
+ - [ ] Handler does NOT contain `Result.validationError()` or any `if (x) return Result.failure(...)` validation
34
+ - [ ] Controller only injects `CommandBus`/`QueryBus` — no services, no repositories
35
+
36
+ ## Controller Pattern (MANDATORY — thin controllers)
37
+
38
+ Controllers ONLY parse the request and dispatch to command/query bus. No services, no repositories, no business logic.
39
+
40
+ ```typescript
41
+ import { Controller, Post, Get, Body, Param } from '@nestjs/common';
42
+ import { CommandBus, QueryBus } from '@nestjs/cqrs';
43
+
44
+ @Controller('items')
45
+ export class ItemsController {
46
+ constructor(
47
+ private readonly commandBus: CommandBus,
48
+ private readonly queryBus: QueryBus,
49
+ ) {}
50
+
51
+ @Post()
52
+ async create(@Body() dto: CreateItemDto) {
53
+ return this.commandBus.execute(new CreateItemCommand(dto.name, dto.description));
54
+ }
55
+
56
+ @Get(':id')
57
+ async findOne(@Param('id') id: string) {
58
+ return this.queryBus.execute(new GetItemByIdQuery(id));
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## CQRS Handler Pattern
64
+
65
+ Every feature is a **Command class + CommandHandler** pair. Controllers are thin — they only
66
+ parse the request and dispatch to the command/query bus.
67
+
68
+ All commands are **transactional by default** (UnitOfWork pattern). Nested commands share one transaction.
69
+
70
+ ```typescript
71
+ import { Validate, DistributedLock, getTransactionalRepo, Result } from '@nestjs-cqrs/quanticjs';
72
+ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
73
+ import { InjectRepository } from '@nestjs/typeorm';
74
+ import { Repository } from 'typeorm';
75
+
76
+ // Command class — MUST have @Validate decorator (without it, .validator.ts is dead code)
77
+ @Validate(CreateItemValidator) // ← MANDATORY — this wires the validator
78
+ @DistributedLock('create-item:{name}')
79
+ export class CreateItemCommand {
80
+ static readonly logExclude = ['largeField']; // optional: exclude fields from @Log output
81
+ constructor(public readonly name: string, public readonly description: string) {}
82
+ }
83
+
84
+ // Handler — uses getTransactionalRepo for UnitOfWork participation
85
+ // ⚠️ NEVER put validation logic in handlers — use @Validate + .validator.ts
86
+ @CommandHandler(CreateItemCommand)
87
+ export class CreateItemHandler implements ICommandHandler<CreateItemCommand> {
88
+ constructor(@InjectRepository(Item) private readonly itemRepo: Repository<Item>) {}
89
+ async execute(command: CreateItemCommand): Promise<Result<ItemDto>> {
90
+ const itemRepo = getTransactionalRepo(this.itemRepo);
91
+ const item = itemRepo.create({ name: command.name, description: command.description });
92
+ await itemRepo.save(item);
93
+ return Result.success(toDto(item));
94
+ }
95
+ }
96
+ ```
97
+
98
+ ## Pipeline Behavior Chains
99
+
100
+ **Commands:** `Log (global) → FeatureFlag → Validate → Cache → DistributedLock → Transactional (auto) → Handler`
101
+ **Queries:** `Log (global) → FeatureFlag → Validate → Cache → Handler`
102
+
103
+ - `@Log` — global, single structured log per command (payload, duration, result, auto-truncated)
104
+ - `@FeatureFlag('flag-name')` — Unleash feature flag check before execution (see Feature Flags section below)
105
+ - `@Validate(ValidatorClass)` — Zod validation via separate `.validator.ts` class
106
+ - `@Cache('key:{prop}', { ttlSeconds: 60 })` — Redis caching with key interpolation
107
+ - `@DistributedLock('key:{prop}')` — Redis distributed lock for concurrency protection
108
+ - **Transactional** — automatic for all commands (UnitOfWork). Nested commands share one tx
109
+ - `@IsolatedTransaction()` — opt-out: runs in own transaction (audit logs, notifications)
110
+
111
+ ### Decorator Reference
112
+ ```typescript
113
+ // @Validate — MANDATORY for ALL commands (without it, the .validator.ts is dead code!)
114
+ @Validate(CreateItemValidator)
115
+ export class CreateItemCommand { ... }
116
+
117
+ // @DistributedLock — concurrency protection
118
+ @DistributedLock('create-item:{name}')
119
+ export class CreateItemCommand { ... }
120
+
121
+ // @Cache — read-heavy queries
122
+ @Cache('items:list:{orgId}', { ttlSeconds: 60 })
123
+ export class ListItemsQuery { ... }
124
+
125
+ // @FeatureFlag — see Feature Flags section for naming + fallback rules
126
+ @FeatureFlag('release-billing-premium-export', { fallback: 'throw' })
127
+ export class ExportReportCommand { ... }
128
+
129
+ // @IsolatedTransaction — independent commit (audit, notifications)
130
+ @IsolatedTransaction()
131
+ export class WriteAuditLogCommand { ... }
132
+ ```
133
+
134
+ ## Validation Pattern (MANDATORY)
135
+
136
+ **Two layers — never mix them:**
137
+
138
+ | Layer | Tool | Where |
139
+ |-------|------|-------|
140
+ | DTO (controller) | class-validator decorators | `*.dto.ts` |
141
+ | Command (pipeline) | Zod + `@Validate(ValidatorClass)` | `*.validator.ts` |
142
+
143
+ **CRITICAL:** Creating a `.validator.ts` file is NOT enough. The command class MUST have `@Validate(XxxValidator)` decorator or the validator never executes.
144
+
145
+ **Handlers MUST NOT contain validation logic.** No `if (x < y) return Result.validationError(...)` in handlers. ALL business rule validation belongs in the Zod validator:
146
+
147
+ ```typescript
148
+ // ❌ WRONG — validation in handler
149
+ async execute(cmd: RegisterUserCommand): Promise<Result<UserDto>> {
150
+ if (calculateAge(cmd.dateOfBirth) < 18) {
151
+ return Result.validationError('Must be at least 18');
152
+ }
153
+ // ...
154
+ }
155
+
156
+ // ✅ CORRECT — validation in .validator.ts via Zod, wired with @Validate on command
157
+ import { z } from 'zod';
158
+ import { ICommandValidator, validateCommand } from '@nestjs-cqrs/quanticjs';
159
+
160
+ export class RegisterUserValidator implements ICommandValidator<RegisterUserCommand> {
161
+ private schema = z.object({
162
+ email: z.string().email(),
163
+ dateOfBirth: z.coerce.date().refine(
164
+ dob => calculateAge(dob) >= 18,
165
+ 'Must be at least 18 years old'
166
+ ),
167
+ password: z.string().min(8).max(128),
168
+ });
169
+ validate(cmd: RegisterUserCommand) { return validateCommand(this.schema, cmd); }
170
+ }
171
+ ```
172
+
173
+ ## UnitOfWork — Nested Command Transactions
174
+
175
+ All commands share an ambient transaction via `AsyncLocalStorage`. Nested commands automatically join the outer transaction — all commit or rollback together.
176
+
177
+ ```typescript
178
+ @CommandHandler(OnboardCustomerCommand)
179
+ export class OnboardCustomerHandler {
180
+ async execute(cmd: OnboardCustomerCommand): Promise<Result<void>> {
181
+ const user = await this.commandBus.execute(new CreateUserCommand(...));
182
+ if (!user.isSuccess) return user; // triggers rollback of everything
183
+ const org = await this.commandBus.execute(new CreateOrgCommand(...));
184
+ if (!org.isSuccess) return org; // triggers rollback of everything (including user)
185
+ return Result.success(); // commits everything atomically
186
+ }
187
+ }
188
+ ```
189
+
190
+ Use `@IsolatedTransaction()` to opt out (audit logs, notifications that must commit independently).
191
+
192
+ ## Result<T> Usage
193
+
194
+ Handlers return `Result<T>` — never throw for business errors.
195
+
196
+ ```typescript
197
+ // Creating results
198
+ Result.success(value) // happy path
199
+ Result.failure(ErrorType.NotFound, 'message') // typed error
200
+ Result.notFound('message') // shorthand
201
+ Result.conflict('message') // shorthand
202
+ Result.forbidden('message') // shorthand
203
+ Result.unauthorized('message') // shorthand
204
+ Result.unprocessableEntity('message') // shorthand
205
+ Result.validationError('message') // ONLY from validators, never handlers
206
+
207
+ // Using results
208
+ result.isSuccess // boolean check
209
+ result.value // access value (undefined if failure)
210
+ result.unwrap() // get value or throw if failure
211
+ result.map(item => toDto(item)) // transform value, preserving error state
212
+ ```
213
+
214
+ ## Entity Patterns
215
+
216
+ All entities extend `BaseEntity` from `@nestjs-cqrs/quanticjs`:
217
+ - `id` (UUID, auto-generated)
218
+ - `createdAt` (timestamp)
219
+ - `updatedAt` (timestamp)
220
+
221
+ Tenant-scoped entities extend `TenantBaseEntity` (adds `organizationId`).
222
+
223
+ ```typescript
224
+ import { BaseEntity, TenantBaseEntity, Result } from '@nestjs-cqrs/quanticjs';
225
+ ```
226
+
227
+ ## Redis Streams — Inter-Module Events
228
+
229
+ - Use `RedisStreamConsumer` base class for consumers (handles connection isolation)
230
+ - Every consumer uses a **dedicated cloned connection** for blocking XREADGROUP
231
+ - Shared `REDIS_CLIENT` is for non-blocking ops only (cache, XADD, locks)
232
+ - `onModuleInit()` for setup; `onApplicationBootstrap()` for polling loops
233
+ - Events published after transaction commits
234
+
235
+ ### Retry and Dead-Letter Policy
236
+
237
+ - **Max retries:** 5 with exponential backoff + jitter (`min(1s × 2^attempt + random(0,1000ms), 30s)`)
238
+ - **Dead-letter stream:** `{stream}:dlq` (e.g., `orders:events:dlq`) with `MAXLEN ~ 100000`
239
+ - Failed events are never silently dropped — retry or dead-letter
240
+
241
+ ## NestJS Module Patterns — Singletons and Lifecycle
242
+
243
+ ### .forRoot() Modules — Import ONCE in app.module.ts
244
+
245
+ Modules with `.forRoot()` (ScheduleModule, LoggerModule, BullModule, etc.) MUST be imported
246
+ **exactly once** in `app.module.ts`. Feature modules import the regular module (no `.forRoot()`).
247
+
248
+ ```typescript
249
+ // ❌ WRONG — .forRoot() in every feature module
250
+ @Module({ imports: [ScheduleModule.forRoot()] })
251
+ export class ActivityModule {}
252
+
253
+ @Module({ imports: [ScheduleModule.forRoot()] })
254
+ export class BillingModule {}
255
+
256
+ // ✅ CORRECT — .forRoot() once in app.module.ts
257
+ @Module({
258
+ imports: [
259
+ ScheduleModule.forRoot(), // once here
260
+ LoggerModule.forRoot(pinoConfig), // once here
261
+ BullModule.forRoot({ redis }), // once here
262
+ ActivityModule,
263
+ BillingModule,
264
+ ],
265
+ })
266
+ export class AppModule {}
267
+
268
+ // Feature modules just use the schedule decorators — no .forRoot() needed
269
+ @Module({ providers: [ActivityCleanupService] })
270
+ export class ActivityModule {}
271
+ ```
272
+
273
+ **Common .forRoot() modules that must be in app.module.ts only:**
274
+ - `ScheduleModule.forRoot()` — @nestjs/schedule
275
+ - `LoggerModule.forRoot()` — nestjs-pino
276
+ - `BullModule.forRoot()` — @nestjs/bull
277
+ - `EventEmitterModule.forRoot()` — @nestjs/event-emitter
278
+ - `ThrottlerModule.forRoot()` — @nestjs/throttler
279
+
280
+ ### Lifecycle Hooks — Setup vs. Async Work
281
+
282
+ **`onModuleInit()`** — for synchronous setup (create consumer groups, register handlers).
283
+ **`onApplicationBootstrap()`** — for starting async work (polling loops, listeners).
284
+
285
+ ```typescript
286
+ // ❌ WRONG — blocking poll loop in onModuleInit prevents app.listen()
287
+ @Injectable()
288
+ export class EventConsumer implements OnModuleInit {
289
+ async onModuleInit() {
290
+ await this.createConsumerGroup();
291
+ while (true) { // 🔥 blocks forever — app never starts
292
+ await this.redis.xreadgroup('GROUP', 'mygroup', 'consumer', 'BLOCK', 5000, ...);
293
+ }
294
+ }
295
+ }
296
+
297
+ // ✅ CORRECT — setup in onModuleInit, polling in onApplicationBootstrap
298
+ @Injectable()
299
+ export class EventConsumer implements OnModuleInit, OnApplicationBootstrap {
300
+ async onModuleInit() {
301
+ // Synchronous setup only — create consumer groups, register handlers
302
+ await this.createConsumerGroup();
303
+ }
304
+
305
+ async onApplicationBootstrap() {
306
+ // App is fully started — safe to begin async polling
307
+ this.startPolling(); // non-blocking — fires and forgets
308
+ }
309
+
310
+ private async startPolling() {
311
+ while (this.isRunning) {
312
+ const messages = await this.redis.xreadgroup(...);
313
+ // process messages
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ **Why this matters:** Multiple Redis stream consumers each calling `XREADGROUP BLOCK` in `onModuleInit()` cascade-block the entire startup. The app never reaches `app.listen()`.
320
+
321
+ ### Redis Connection Isolation — Blocking vs. Non-Blocking
322
+
323
+ `XREADGROUP BLOCK` monopolizes the TCP connection for the block duration. If consumers share
324
+ the `REDIS_CLIENT` singleton, non-blocking callers (cache GET/SET, distributed lock SET NX,
325
+ XADD publish) queue behind the blocked read — causing multi-second latency spikes.
326
+
327
+ **Rule:** Every stream consumer MUST use a **dedicated cloned connection** for blocking reads.
328
+ The shared `REDIS_CLIENT` is for non-blocking ops only.
329
+
330
+ ```typescript
331
+ // ❌ WRONG — blocking read on the shared client starves cache/locks
332
+ @Injectable()
333
+ export class EventConsumer {
334
+ constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {}
335
+
336
+ async poll() {
337
+ await this.redis.xreadgroup('GROUP', 'g', 'c', 'BLOCK', 5000, 'STREAMS', 'key', '>');
338
+ }
339
+ }
340
+
341
+ // ✅ CORRECT — RedisStreamConsumer base class handles this automatically
342
+ // It clones a dedicated connection via redis.duplicate() for blocking XREADGROUP,
343
+ // while using the shared REDIS_CLIENT for XGROUP CREATE, XACK, and other non-blocking ops.
344
+ @Injectable()
345
+ export class EventConsumer extends RedisStreamConsumer {
346
+ // Just extend RedisStreamConsumer — connection isolation is built in
347
+ }
348
+ ```
349
+
350
+ **Connection budget:** 1 shared `REDIS_CLIENT` + 1 dedicated connection per consumer.
351
+ For a service with 3 consumers, that's 4 total Redis connections.
352
+
353
+ ### Module Exports — Only Export What You Provide
354
+
355
+ A module can only export providers it declares or imports. Exporting a class that isn't a provider in the module causes a runtime error.
356
+
357
+ ```typescript
358
+ // ❌ WRONG — RedisStreamPublisher not declared as provider
359
+ @Module({
360
+ providers: [EventStreamService],
361
+ exports: [EventStreamService, RedisStreamPublisher], // 🔥 RedisStreamPublisher not provided
362
+ })
363
+ export class EventBusModule {}
364
+
365
+ // ✅ CORRECT — only export what's in providers (or re-exported modules)
366
+ @Module({
367
+ providers: [EventStreamService],
368
+ exports: [EventStreamService],
369
+ })
370
+ export class EventBusModule {}
371
+ ```
372
+
373
+ ## Feature Flags
374
+
375
+ Uses `@FeatureFlag()` decorator with Unleash. Three categories:
376
+
377
+ | Category | Naming | Lifetime | Default fallback |
378
+ |---|---|---|---|
379
+ | **Release** | `release-{module}-{feature}` | Remove within 30 days of full rollout | `throw` (Forbidden) |
380
+ | **Kill switch** | `kill-{module}-{feature}` | Permanent — stays in code | `throw` (Forbidden) |
381
+ | **Experiment** | `experiment-{module}-{feature}` | Remove within 90 days | `default` (control variant) |
382
+
383
+ **Fallback strategies** when a flag is disabled:
384
+
385
+ | Strategy | Behavior |
386
+ |---|---|
387
+ | `throw` (default) | Returns `Result.forbidden('Feature disabled: {flag}')` |
388
+ | `skip` | Returns `Result.success(undefined)` — for optional features |
389
+ | `default` | Returns `Result.success(defaultValue)` — for experiments |
390
+
391
+ ```typescript
392
+ @FeatureFlag('release-billing-invoices') // throw if disabled
393
+ @FeatureFlag('kill-payments-processing') // throw if disabled
394
+ @FeatureFlag('experiment-scoring-v2', { fallback: 'default', defaultValue: oldResult })
395
+ @FeatureFlag('release-notifications-email', { fallback: 'skip' }) // silently skip
396
+ ```
397
+
398
+ **Graceful degradation:** If `UNLEASH_URL` is not set, all flags pass (features enabled by default). Local dev and tests work without Unleash.
399
+
400
+ ## NEVER
401
+
402
+ - **NEVER** inject services or repositories into controllers — dispatch to the bus only
403
+ - **NEVER** put business logic in controllers
404
+ - **NEVER** put validation logic in handlers — use `@Validate` + `.validator.ts`
405
+ - **NEVER** use Joi, Yup, or other validation libraries — class-validator for DTOs, Zod for commands
406
+ - **NEVER** create a `.validator.ts` file without `@Validate(XxxValidator)` on the command class — it's dead code
407
+ - **NEVER** throw `HttpException` from handlers — return `Result<T>`
408
+ - **NEVER** use `Result.validationError()` in handlers
409
+ - **NEVER** ignore Result values from nested commands
410
+ - **NEVER** call `.unwrap()` inside handlers
411
+ - **NEVER** manually manage transactions — use `getTransactionalRepo()`
412
+ - **NEVER** share Redis connection for blocking XREADGROUP reads
413
+ - **NEVER** start blocking poll loops in `onModuleInit()` — use `onApplicationBootstrap()`
414
+ - **NEVER** publish events before the transaction commits
415
+ - **NEVER** silently drop failed stream events — retry with backoff or dead-letter
416
+ - **NEVER** use `NestJS EventEmitter` for inter-module events
417
+ - **NEVER** use global singletons for shared business state across modules — use Redis or the database
418
+ - **NEVER** import `.forRoot()` modules in feature modules
419
+ - **NEVER** use feature flags on infrastructure code (migrations, middleware, module config) — use env vars
420
+ - **NEVER** nest multiple `@FeatureFlag` decorators on one handler — one handler, one flag
421
+ - **NEVER** use flags as permanent configuration — if always needs on/off, use env vars