@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.
- package/dist/deps.d.ts +4 -0
- package/dist/deps.js +97 -0
- package/dist/deps.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.js +20 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/bff.d.ts +2 -0
- package/dist/generators/bff.js +9 -0
- package/dist/generators/bff.js.map +1 -0
- package/dist/generators/claude.d.ts +2 -0
- package/dist/generators/claude.js +45 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.js +10 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/e2e.d.ts +2 -0
- package/dist/generators/e2e.js +8 -0
- package/dist/generators/e2e.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.js +28 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/module.d.ts +2 -0
- package/dist/generators/module.js +35 -0
- package/dist/generators/module.js.map +1 -0
- package/dist/generators/root.d.ts +2 -0
- package/dist/generators/root.js +7 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/scripts.d.ts +2 -0
- package/dist/generators/scripts.js +10 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +8 -0
- package/dist/prompts.js +53 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +79 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/utils/exec.d.ts +2 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/template.d.ts +10 -0
- package/dist/utils/template.js +20 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validate.d.ts +4 -0
- package/dist/utils/validate.js +27 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +50 -0
- package/templates/backend/app.module.ts.ejs +61 -0
- package/templates/backend/bff.controller.ts.ejs +60 -0
- package/templates/backend/bff.module.ts.ejs +10 -0
- package/templates/backend/bff.service.ts.ejs +6 -0
- package/templates/backend/create-schema.migration.ts.ejs +15 -0
- package/templates/backend/data-source.ts.ejs +13 -0
- package/templates/backend/env.example.ejs +30 -0
- package/templates/backend/main.ts.ejs +43 -0
- package/templates/backend/module.ts.ejs +14 -0
- package/templates/backend/nest-cli.json.ejs +8 -0
- package/templates/backend/package.json.ejs +23 -0
- package/templates/backend/tsconfig.build.json.ejs +4 -0
- package/templates/backend/tsconfig.json.ejs +24 -0
- package/templates/claude/CLAUDE.md.ejs +86 -0
- package/templates/claude/hooks/auto-format.sh +22 -0
- package/templates/claude/hooks/check-secrets.sh +49 -0
- package/templates/claude/hooks/guard-destructive.sh +42 -0
- package/templates/claude/hooks/on-compaction.sh +29 -0
- package/templates/claude/mcp.json +10 -0
- package/templates/claude/rules/api-patterns.md +86 -0
- package/templates/claude/rules/auth-patterns.md +109 -0
- package/templates/claude/rules/backend-patterns.md +421 -0
- package/templates/claude/rules/database-patterns.md +96 -0
- package/templates/claude/rules/docker-patterns.md +86 -0
- package/templates/claude/rules/frontend-patterns.md +262 -0
- package/templates/claude/rules/observability-backend.md +132 -0
- package/templates/claude/rules/observability-frontend.md +49 -0
- package/templates/claude/rules/playwright-mcp.md +80 -0
- package/templates/claude/rules/resilience-ops.md +103 -0
- package/templates/claude/rules/testing-e2e-ui.md +190 -0
- package/templates/claude/rules/testing-patterns.md +94 -0
- package/templates/claude/rules/workflow-backend.md +64 -0
- package/templates/claude/rules/workflow-frontend.md +60 -0
- package/templates/claude/settings.json +68 -0
- package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
- package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
- package/templates/claude/skills/add-entity/SKILL.md +56 -0
- package/templates/claude/skills/add-event/SKILL.md +127 -0
- package/templates/claude/skills/add-feature/SKILL.md +20 -0
- package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
- package/templates/claude/skills/add-handler/SKILL.md +105 -0
- package/templates/claude/skills/add-integration/SKILL.md +176 -0
- package/templates/claude/skills/add-migration/SKILL.md +20 -0
- package/templates/claude/skills/add-module/SKILL.md +89 -0
- package/templates/claude/skills/add-realtime/SKILL.md +119 -0
- package/templates/claude/skills/audit-rules/SKILL.md +120 -0
- package/templates/claude/skills/debugging/SKILL.md +105 -0
- package/templates/claude/skills/docker-dev/SKILL.md +86 -0
- package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
- package/templates/claude/skills/e2e-full/SKILL.md +132 -0
- package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
- package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
- package/templates/claude/skills/fix-bug/SKILL.md +33 -0
- package/templates/claude/skills/implement-spec/SKILL.md +98 -0
- package/templates/claude/skills/review-code/SKILL.md +109 -0
- package/templates/claude/skills/review-spec/SKILL.md +216 -0
- package/templates/claude/skills/run-tests/SKILL.md +37 -0
- package/templates/claude/skills/specify/SKILL.md +87 -0
- package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
- package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
- package/templates/docker/Dockerfile.client.ejs +14 -0
- package/templates/docker/Dockerfile.ejs +28 -0
- package/templates/docker/docker-compose.test.yml.ejs +54 -0
- package/templates/docker/docker-compose.yml.ejs +76 -0
- package/templates/docker/nginx.conf.ejs +21 -0
- package/templates/frontend/App.tsx.ejs +64 -0
- package/templates/frontend/DashboardPage.tsx.ejs +37 -0
- package/templates/frontend/LoginPage.tsx.ejs +20 -0
- package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
- package/templates/frontend/api-client.ts.ejs +15 -0
- package/templates/frontend/index.css.ejs +57 -0
- package/templates/frontend/index.html.ejs +13 -0
- package/templates/frontend/main.tsx.ejs +10 -0
- package/templates/frontend/package.json.ejs +16 -0
- package/templates/frontend/playwright.config.ts.ejs +20 -0
- package/templates/frontend/postcss.config.js.ejs +3 -0
- package/templates/frontend/smoke.spec.ts.ejs +37 -0
- package/templates/frontend/tailwind.config.ts.ejs +56 -0
- package/templates/frontend/tsconfig.json.ejs +25 -0
- package/templates/frontend/tsconfig.node.json.ejs +15 -0
- package/templates/frontend/utils.ts.ejs +6 -0
- package/templates/frontend/vite-env.d.ts.ejs +1 -0
- package/templates/frontend/vite.config.ts.ejs +20 -0
- package/templates/root/gitignore.ejs +9 -0
- package/templates/root/prettierrc.ejs +7 -0
- package/templates/scripts/init-db.sh.ejs +8 -0
- package/templates/scripts/save-auth-state.ts.ejs +24 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "src/**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Database Patterns
|
|
6
|
+
|
|
7
|
+
## TypeORM Code-First Migrations
|
|
8
|
+
|
|
9
|
+
Generate migrations from entity changes — never write SQL by hand:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx typeorm migration:generate src/migrations/AddItemTable
|
|
13
|
+
npx typeorm migration:run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Schema Per Service
|
|
17
|
+
|
|
18
|
+
Each service gets its own PostgreSQL schema (e.g., `identity`, `billing`).
|
|
19
|
+
Migrations reference the schema explicitly.
|
|
20
|
+
|
|
21
|
+
## CRITICAL: TypeORM Uses camelCase Column Names
|
|
22
|
+
|
|
23
|
+
TypeORM's default naming strategy maps entity properties directly to column names.
|
|
24
|
+
**Column names in the database are camelCase, NOT snake_case.**
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Entity property: displayLatitude
|
|
28
|
+
// Database column: "displayLatitude" (NOT display_latitude)
|
|
29
|
+
|
|
30
|
+
// ❌ WRONG
|
|
31
|
+
CREATE INDEX idx_post_lat ON activity.posts ("display_latitude");
|
|
32
|
+
|
|
33
|
+
// ✅ CORRECT
|
|
34
|
+
CREATE INDEX idx_post_lat ON activity.posts ("displayLatitude");
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When referencing tables in raw SQL, use `schema.tableName` (NOT `"schema"."tableName"` with the schema quoted):
|
|
38
|
+
```sql
|
|
39
|
+
-- ❌ WRONG
|
|
40
|
+
SELECT * FROM "activity"."post";
|
|
41
|
+
|
|
42
|
+
-- ✅ CORRECT
|
|
43
|
+
SELECT * FROM activity.posts;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Entity Index Patterns — No Duplicates
|
|
47
|
+
|
|
48
|
+
Use EITHER class-level `@Index` OR property-level `@Index`, never both for the same column.
|
|
49
|
+
|
|
50
|
+
## Migration SQL Rules
|
|
51
|
+
|
|
52
|
+
### CREATE INDEX CONCURRENTLY — Non-Transactional Migrations Only
|
|
53
|
+
|
|
54
|
+
TypeORM migrations run inside transactions by default. `CONCURRENTLY` cannot run in a transaction. For normal migrations, use regular `CREATE INDEX`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// ❌ WRONG — CONCURRENTLY inside a default (transactional) migration
|
|
58
|
+
await queryRunner.query(`CREATE INDEX CONCURRENTLY idx_name ON schema.table ("column")`);
|
|
59
|
+
|
|
60
|
+
// ✅ CORRECT — regular index in a transactional migration
|
|
61
|
+
await queryRunner.query(`CREATE INDEX idx_name ON schema.table ("column")`);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For large tables (millions of rows) where locking must be avoided, create a **separate migration file** with `transaction = false`:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export class AddIndexOnOrderLocationConcurrently1234567890 implements MigrationInterface {
|
|
68
|
+
transaction = false as const;
|
|
69
|
+
|
|
70
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
71
|
+
await queryRunner.query(
|
|
72
|
+
`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_order_location ON catalog.orders ("latitude", "longitude")`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
77
|
+
await queryRunner.query(`DROP INDEX IF EXISTS catalog.idx_order_location`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### NEVER Write Hand-Crafted Migrations That Duplicate Entities
|
|
83
|
+
|
|
84
|
+
If a TypeORM entity defines a table, run `migration:generate`. Do NOT also write a manual `CREATE TABLE`.
|
|
85
|
+
|
|
86
|
+
## Migration Naming Convention
|
|
87
|
+
|
|
88
|
+
Use descriptive names: `AddItemTable`, `AddStatusColumnToOrder`, `CreateIndexOnEmail`.
|
|
89
|
+
|
|
90
|
+
## NEVER
|
|
91
|
+
|
|
92
|
+
- **NEVER** use `synchronize: true` in staging or production
|
|
93
|
+
- **NEVER** write snake_case column names
|
|
94
|
+
- **NEVER** use `CREATE INDEX CONCURRENTLY` inside transactional migrations
|
|
95
|
+
- **NEVER** write manual `CREATE TABLE` for tables with TypeORM entities
|
|
96
|
+
- **NEVER** access another module's tables directly
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "Dockerfile, client/Dockerfile, docker-compose*.yml, scripts/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Docker Patterns
|
|
6
|
+
|
|
7
|
+
## Local Dev Uses Docker Compose (Internal Network)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Daily development
|
|
11
|
+
docker compose up # Start infra + backend (watch mode)
|
|
12
|
+
cd client && npm run dev # Start Vite natively (separate terminal)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Architecture: Modular Monolith — Two Dockerfiles
|
|
16
|
+
|
|
17
|
+
| Service | Dockerfile | What it builds |
|
|
18
|
+
|---------|-----------|----------------|
|
|
19
|
+
| Backend (NestJS) | `Dockerfile` (project root) | All backend modules in one image |
|
|
20
|
+
| Frontend (React) | `client/Dockerfile` | Vite build → nginx (K8s only) |
|
|
21
|
+
|
|
22
|
+
## docker-compose.yml Design
|
|
23
|
+
|
|
24
|
+
- **Only `backend` and `keycloak` have `ports:` sections** (3000, 8080)
|
|
25
|
+
- **Backend uses Docker hostnames** (`postgres`, `redis`, `keycloak`) — NOT `localhost`
|
|
26
|
+
- **Source code is volume-mounted** (`./src:/app/src:cached`)
|
|
27
|
+
- **Keycloak uses dev-mem mode** for fast startup
|
|
28
|
+
- **Database init** handled by `scripts/init-db.sh`
|
|
29
|
+
|
|
30
|
+
## Backend Dockerfile (multi-stage)
|
|
31
|
+
|
|
32
|
+
```dockerfile
|
|
33
|
+
# Development target — used by docker-compose.yml
|
|
34
|
+
FROM node:20-alpine AS development
|
|
35
|
+
WORKDIR /app
|
|
36
|
+
COPY package*.json ./
|
|
37
|
+
RUN npm ci
|
|
38
|
+
COPY tsconfig*.json nest-cli.json ./
|
|
39
|
+
# src/ volume-mounted at runtime
|
|
40
|
+
CMD ["npm", "run", "start:dev"]
|
|
41
|
+
|
|
42
|
+
# Production target
|
|
43
|
+
FROM node:20-alpine AS production
|
|
44
|
+
WORKDIR /app
|
|
45
|
+
ENV NODE_ENV=production
|
|
46
|
+
COPY package*.json ./
|
|
47
|
+
RUN npm ci --only=production && npm cache clean --force
|
|
48
|
+
COPY --from=builder /app/dist ./dist
|
|
49
|
+
CMD ["node", "dist/main.js"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Vite Proxy Configuration (critical for BFF auth)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// client/vite.config.ts
|
|
56
|
+
export default defineConfig({
|
|
57
|
+
server: {
|
|
58
|
+
proxy: {
|
|
59
|
+
'/api': { target: 'http://localhost:3000', changeOrigin: true },
|
|
60
|
+
'/auth': { target: 'http://localhost:3000', changeOrigin: true },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Kubernetes (Integration Testing Only)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
scripts/local-dev-up.sh # Create Kind cluster + deploy
|
|
70
|
+
scripts/helm-deploy-local.sh # Re-deploy after Helm changes
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use Helm release-prefixed service names, not Docker Compose short names.
|
|
74
|
+
|
|
75
|
+
## NEVER
|
|
76
|
+
|
|
77
|
+
- **NEVER** expose infrastructure ports to the host (except Keycloak 8080 for OIDC redirects)
|
|
78
|
+
- **NEVER** run Vite inside Docker — HMR is unreliable with volume mounts
|
|
79
|
+
- **NEVER** create per-service Dockerfiles — this is a monolith
|
|
80
|
+
- **NEVER** run services as root in production images
|
|
81
|
+
- **NEVER** copy `node_modules/` into the image — always `npm ci`
|
|
82
|
+
- **NEVER** use `docker compose up` for E2E or integration tests — use `docker compose -f docker-compose.test.yml up` (isolated ports: API 3099, Keycloak 8099, Frontend 5199)
|
|
83
|
+
- **NEVER** hardcode API URLs in frontend — use relative paths (`/api/items`)
|
|
84
|
+
- **NEVER** use Docker Compose short hostnames in Helm values
|
|
85
|
+
- **NEVER** mount host `node_modules` into Kubernetes pods
|
|
86
|
+
- **NEVER** use local K8s for daily feature development — use Docker Compose
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "client/**/*.{ts,tsx}"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Frontend Patterns
|
|
6
|
+
|
|
7
|
+
## State Management — Decision Tree
|
|
8
|
+
|
|
9
|
+
| State type | Tool | When |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Server/remote data** | TanStack Query (`useQuery` / `useMutation`) | Data from API |
|
|
12
|
+
| **URL-derived state** | `useSearchParams` (React Router) | Filters, tabs, pagination — anything bookmarkable |
|
|
13
|
+
| **Local UI state** | `useState` | Open/closed, hover, animation — never leaves the component |
|
|
14
|
+
| **Shared client state** | Zustand store | Client-only state needed by 2+ unrelated components |
|
|
15
|
+
| **Form state** | `useForm` (`@quanticjs/react-forms`) + Zod | Multi-field forms with validation + automatic server error mapping |
|
|
16
|
+
|
|
17
|
+
### TanStack Query — All API Calls
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
const { data, isLoading } = useQuery({
|
|
21
|
+
queryKey: ['items', id],
|
|
22
|
+
queryFn: () => api.getItem(id),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Same `queryKey` in multiple components = one network request (auto-dedup).
|
|
27
|
+
|
|
28
|
+
### Zustand — Selectors Only
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ CORRECT — only re-renders when radius changes
|
|
32
|
+
const radius = useFilterStore((s) => s.radius);
|
|
33
|
+
|
|
34
|
+
// ❌ WRONG — re-renders on ANY store change
|
|
35
|
+
const { radius, category } = useFilterStore();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### URL State
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
42
|
+
const tab = searchParams.get('tab') ?? 'discover';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API Error Handling
|
|
46
|
+
|
|
47
|
+
The backend returns RFC 9457 problem-details JSON on all errors. The API client parses these into `ApiError` instances. Three tiers handle them: forms → toast → root ErrorBoundary (see provider stack below).
|
|
48
|
+
|
|
49
|
+
### Error classification
|
|
50
|
+
|
|
51
|
+
| ApiError property | HTTP status | UI behavior |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `isValidation` | 400, 422 | Field errors on form, or detailed toast |
|
|
54
|
+
| `isNotFound` | 404 | Navigate to "not found" page or show empty state |
|
|
55
|
+
| `isForbidden` | 403 | Show "access denied" — do NOT retry |
|
|
56
|
+
| `isUnauthorized` | 401 | Automatic — refresh token → retry → redirect to login |
|
|
57
|
+
| `isConflict` | 409 | Show "already exists" or "was modified" — suggest refresh |
|
|
58
|
+
| 5xx | 500, 502, 503 | Generic "Something went wrong" toast — **never** show `error.detail` (may contain stack traces) |
|
|
59
|
+
|
|
60
|
+
Always include `error.correlationId` in error UI so users can report it to support.
|
|
61
|
+
|
|
62
|
+
### Forms — automatic server-to-field mapping
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const { register, handleSubmit, errors } = useForm({
|
|
66
|
+
schema: z.object({ name: z.string().min(1), email: z.string().email() }),
|
|
67
|
+
onSubmit: async (data) => await api.post('/items', data),
|
|
68
|
+
});
|
|
69
|
+
// Server { errors: { email: "already taken" } } → errors.email auto-set
|
|
70
|
+
// Non-field errors → errors._root
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Non-form mutations — MANDATORY `onError`
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const mutation = useMutation({
|
|
77
|
+
mutationFn: (id: string) => api.delete(`/items/${id}`),
|
|
78
|
+
onError: (error) => {
|
|
79
|
+
if (error instanceof ApiError) {
|
|
80
|
+
toast.error(error); // ApiError-aware — extracts title + detail
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## App Root Provider Stack (MANDATORY)
|
|
87
|
+
|
|
88
|
+
The app root must wrap providers in this exact order. Outer providers are available to inner ones.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Sentry.ErrorBoundary fallback={<ErrorFallback />}>
|
|
92
|
+
<QuanticProvider client={apiClient}>
|
|
93
|
+
<QuanticQueryProvider>
|
|
94
|
+
<ToastProvider>
|
|
95
|
+
<ErrorBoundary
|
|
96
|
+
fallback={(error, reset) => <AppErrorPage error={error} onRetry={reset} />}
|
|
97
|
+
onError={(error) => Sentry.captureException(error)}
|
|
98
|
+
>
|
|
99
|
+
<RouterProvider router={router} />
|
|
100
|
+
</ErrorBoundary>
|
|
101
|
+
</ToastProvider>
|
|
102
|
+
</QuanticQueryProvider>
|
|
103
|
+
</QuanticProvider>
|
|
104
|
+
</Sentry.ErrorBoundary>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Why this order:**
|
|
108
|
+
- `Sentry.ErrorBoundary` outermost — catches errors even if inner providers fail to mount
|
|
109
|
+
- `QuanticProvider` — API client context, needed by everything below
|
|
110
|
+
- `QuanticQueryProvider` — TanStack Query with smart defaults (no 4xx retry, 30s stale time)
|
|
111
|
+
- `ToastProvider` — toast context must wrap ErrorBoundary so error fallbacks can show toasts
|
|
112
|
+
- `ErrorBoundary` — catches render errors, shows recoverable UI, reports to Sentry
|
|
113
|
+
- `RouterProvider` innermost — pages and components
|
|
114
|
+
|
|
115
|
+
## shadcn/ui Components
|
|
116
|
+
|
|
117
|
+
Use shadcn/ui primitives from `@/components/ui`. Compose into feature components — never modify primitives directly. All components must accept `className` for extension and forward refs.
|
|
118
|
+
|
|
119
|
+
## Component Library Requirements
|
|
120
|
+
|
|
121
|
+
- Every shared component must have a Storybook entry showing variants, states (loading, disabled, error), and usage
|
|
122
|
+
- Accessibility enforced in CI via axe-core at WCAG 2.1 Level AA — violations block PR merges
|
|
123
|
+
|
|
124
|
+
## Dark Mode
|
|
125
|
+
|
|
126
|
+
Use CSS variables: `hsl(var(--background))`, `hsl(var(--primary))`, etc.
|
|
127
|
+
|
|
128
|
+
## TypeScript Strict Mode
|
|
129
|
+
|
|
130
|
+
All code uses `strict: true`, `noUncheckedIndexedAccess: true`. No `any`, no `@ts-ignore`.
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"compilerOptions": {
|
|
135
|
+
"strict": true,
|
|
136
|
+
"noUncheckedIndexedAccess": true,
|
|
137
|
+
"noImplicitOverride": true,
|
|
138
|
+
"noFallthroughCasesInSwitch": true,
|
|
139
|
+
"forceConsistentCasingInFileNames": true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Browser Type Safety
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// ❌ WRONG — requires @types/node
|
|
148
|
+
const timer: NodeJS.Timeout = setTimeout(() => {}, 1000);
|
|
149
|
+
|
|
150
|
+
// ✅ CORRECT
|
|
151
|
+
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {}, 1000);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Page Components
|
|
155
|
+
|
|
156
|
+
All page components are lazy-loaded with `React.lazy()` and wrapped in `<Suspense>`.
|
|
157
|
+
|
|
158
|
+
## Design Tokens
|
|
159
|
+
|
|
160
|
+
No hardcoded hex values in application code. All visual values come from design tokens/CSS variables.
|
|
161
|
+
|
|
162
|
+
## QuanticJS Framework Usage
|
|
163
|
+
|
|
164
|
+
All frontend code uses framework packages from `@quanticjs/*`. These are the canonical tools — do not introduce alternatives.
|
|
165
|
+
|
|
166
|
+
### Packages
|
|
167
|
+
|
|
168
|
+
| Package | Purpose | Used by |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `@quanticjs/react-core` | API client, `ApiError`, `useClient`, `usePermissions`, `QuanticProvider`, `QuanticQueryProvider` | App roots, data hooks |
|
|
171
|
+
| `@quanticjs/react-forms` | `useForm` with Zod + automatic server error mapping | All forms |
|
|
172
|
+
| `@quanticjs/workflow-ui` | `WorkflowProvider`, task hooks (`useTaskList`, `useTask`, `useTaskClaim`, `useTaskAction`, `useProcessTimeline`), `TaskInbox`, `WorkflowForm`, `TaskDetail`, `TaskActions`, `ProcessTimeline` | Workflow apps |
|
|
173
|
+
|
|
174
|
+
### API Client and Errors
|
|
175
|
+
|
|
176
|
+
Use `useClient()` from `@quanticjs/react-core` for API calls. The client parses RFC 9457 responses into `ApiError` instances.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { useClient, ApiError, isApiError } from '@quanticjs/react-core';
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Mutation Error Handling
|
|
183
|
+
|
|
184
|
+
Every `useMutation` must have an `onError` callback that uses `ApiError`-aware toasting:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
onError: (error) => {
|
|
188
|
+
if (error instanceof ApiError) {
|
|
189
|
+
toast({
|
|
190
|
+
title: error.isServerError ? 'Something went wrong' : error.title,
|
|
191
|
+
description: error.isServerError
|
|
192
|
+
? `Please try again. (ref: ${error.correlationId ?? 'unknown'})`
|
|
193
|
+
: `${error.detail ?? ''} (ref: ${error.correlationId ?? 'unknown'})`,
|
|
194
|
+
variant: 'destructive',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Toast Provider
|
|
201
|
+
|
|
202
|
+
Use the app's `useToast()` hook (from `@/components/toast-provider` or `WorkflowProvider`). Toasts always include `correlationId` for error cases.
|
|
203
|
+
|
|
204
|
+
### Library Components (`@quanticjs/workflow-ui`)
|
|
205
|
+
|
|
206
|
+
All exported components MUST:
|
|
207
|
+
- Accept `className` prop for style extension
|
|
208
|
+
- Forward refs where applicable
|
|
209
|
+
- Use CSS variables / Tailwind semantic classes — no hardcoded hex or spacing values
|
|
210
|
+
- No `console.log` / `console.warn` — use Sentry or structured error reporting
|
|
211
|
+
|
|
212
|
+
### Third-Party Library Integration (bpmn-js)
|
|
213
|
+
|
|
214
|
+
bpmn-js lacks TypeScript types. All `any` casts must be isolated behind a typed adapter module (`bpmn-types.ts`) that provides typed wrappers for `modeler.get()`:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// bpmn-types.ts — centralized typed access
|
|
218
|
+
import type Modeler from 'bpmn-js/lib/Modeler';
|
|
219
|
+
import type Viewer from 'bpmn-js/lib/Viewer';
|
|
220
|
+
|
|
221
|
+
interface BpmnCanvas { zoom(level: number | 'fit-viewport'): number; addMarker(id: string, cls: string): void; removeMarker(id: string, cls: string): void; scrollToElement(el: BpmnElement): void; }
|
|
222
|
+
interface BpmnCommandStack { canUndo(): boolean; canRedo(): boolean; undo(): void; redo(): void; }
|
|
223
|
+
interface BpmnElementRegistry { get(id: string): BpmnElement | undefined; }
|
|
224
|
+
interface BpmnModeling { updateProperties(element: BpmnElement, props: Record<string, unknown>): void; }
|
|
225
|
+
interface BpmnSelection { select(element: BpmnElement): void; }
|
|
226
|
+
interface BpmnOverlays { add(id: string, type: string, overlay: unknown): void; remove(filter: Record<string, unknown>): void; }
|
|
227
|
+
interface BpmnModdle { create(type: string, props?: Record<string, unknown>): unknown; }
|
|
228
|
+
export interface BpmnElement { id: string; type: string; businessObject?: Record<string, unknown>; width?: number; height?: number; source?: BpmnElement; [key: string]: unknown; }
|
|
229
|
+
|
|
230
|
+
type ServiceMap = { canvas: BpmnCanvas; commandStack: BpmnCommandStack; elementRegistry: BpmnElementRegistry; modeling: BpmnModeling; selection: BpmnSelection; overlays: BpmnOverlays; moddle: BpmnModdle; };
|
|
231
|
+
|
|
232
|
+
export function getService<K extends keyof ServiceMap>(modeler: Modeler | Viewer, name: K): ServiceMap[K] {
|
|
233
|
+
return (modeler as Record<string, unknown>).get(name) as ServiceMap[K];
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## NEVER
|
|
238
|
+
|
|
239
|
+
- **NEVER** fetch data with `useEffect` + `useState` — use TanStack Query
|
|
240
|
+
- **NEVER** copy query data into `useState` — it creates a stale snapshot
|
|
241
|
+
- **NEVER** mirror URL params into `useState` — read from `useSearchParams` directly
|
|
242
|
+
- **NEVER** put server data in Zustand — use TanStack Query
|
|
243
|
+
- **NEVER** destructure entire Zustand store without selectors
|
|
244
|
+
- **NEVER** add Redux, MobX, Jotai, Recoil, or Valtio
|
|
245
|
+
- **NEVER** use `console.log` in production code — use Sentry
|
|
246
|
+
- **NEVER** use `any` — use `unknown` and narrow
|
|
247
|
+
- **NEVER** use `@ts-ignore` — use `@ts-expect-error` with comment
|
|
248
|
+
- **NEVER** use `NodeJS.Timeout` or other Node.js types in frontend code
|
|
249
|
+
- **NEVER** hardcode hex colors or spacing values — use design tokens
|
|
250
|
+
- **NEVER** prop-drill through components that don't use the prop
|
|
251
|
+
- **NEVER** parse API error responses manually — use `ApiError` properties (`detail`, `fieldErrors`, `correlationId`)
|
|
252
|
+
- **NEVER** show raw error messages to users in production — map `ApiError.status` to user-friendly messages
|
|
253
|
+
- **NEVER** show `error.detail` from 5xx responses — may contain stack traces; use generic message instead
|
|
254
|
+
- **NEVER** write `catch (e) { console.log(e) }` on mutations — swallowing errors silently is a bug
|
|
255
|
+
- **NEVER** omit `onError` on non-form mutations — every mutation failure must be visible to the user
|
|
256
|
+
- **PREFER** `<ErrorBoundary>` on page components — allows recovering a single page without resetting the entire app (the root boundary is the fallback)
|
|
257
|
+
- **NEVER** use `react-hook-form` directly — use `useForm` from `@quanticjs/react-forms` which adds automatic server error mapping
|
|
258
|
+
- **NEVER** manually map server validation errors to form fields — `useForm` does this automatically via `ApiError.fieldErrors`
|
|
259
|
+
- **NEVER** scatter `(modeler as any).get(...)` across components — isolate all bpmn-js `any` casts in a single `bpmn-types.ts` adapter module
|
|
260
|
+
- **NEVER** use `console.warn` or `console.error` in library packages — use Sentry or structured error reporting
|
|
261
|
+
- **NEVER** export components from `@quanticjs/*` packages without `className` prop support
|
|
262
|
+
- **NEVER** use inline `style={{ gap: '1rem' }}` — use Tailwind gap utilities (`gap-4`) or CSS variables
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "src/**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Backend Observability
|
|
6
|
+
|
|
7
|
+
## Three Pillars
|
|
8
|
+
|
|
9
|
+
| Pillar | Tool | Purpose |
|
|
10
|
+
|--------|------|---------|
|
|
11
|
+
| **Logging** | ELK via `nestjs-pino` | Structured JSON logs with correlation IDs |
|
|
12
|
+
| **Metrics** | Prometheus + Grafana | Latency, error rates, Redis stream lag |
|
|
13
|
+
| **Tracing** | OpenTelemetry → Elasticsearch APM | Distributed traces across HTTP, CQRS, Redis |
|
|
14
|
+
|
|
15
|
+
## Structured Logging
|
|
16
|
+
|
|
17
|
+
All logging uses Pino (`nestjs-pino`). Every request gets a `requestId` propagated through the CQRS pipeline. All logs must be structured JSON (key-value pairs), not string interpolation.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ✅ CORRECT
|
|
21
|
+
this.logger.info({ userId }, 'User created');
|
|
22
|
+
|
|
23
|
+
// ❌ WRONG
|
|
24
|
+
this.logger.info('User created: ' + userId);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Log levels:**
|
|
28
|
+
|
|
29
|
+
| Level | When |
|
|
30
|
+
|-------|------|
|
|
31
|
+
| `error` | Unrecoverable failures — DB down, DLQ events, uncaught exceptions |
|
|
32
|
+
| `warn` | Degraded operation — retry triggered, cache miss on hot path, slow query (>1s) |
|
|
33
|
+
| `info` | Normal operation — request start/end, command executed, event published |
|
|
34
|
+
| `debug` | Dev-only — query params, cache key computed, lock acquired |
|
|
35
|
+
|
|
36
|
+
## CQRS Pipeline Logging
|
|
37
|
+
|
|
38
|
+
Automatic via `LogBehavior`: one structured entry per command/query with name, duration, result, correlationId, userId.
|
|
39
|
+
|
|
40
|
+
## Sensitive Field Handling (LogBehavior)
|
|
41
|
+
|
|
42
|
+
The `LogBehavior` pipeline step handles sensitive data automatically at three levels:
|
|
43
|
+
|
|
44
|
+
**1. Built-in PII masking (automatic — no configuration needed):**
|
|
45
|
+
|
|
46
|
+
These fields are auto-detected and masked in every command/query payload:
|
|
47
|
+
|
|
48
|
+
| Field name | Masking |
|
|
49
|
+
|------------|---------|
|
|
50
|
+
| `email` | `j***@example.com` (first char + domain) |
|
|
51
|
+
| `password` | `[REDACTED]` |
|
|
52
|
+
| `token` | `[REDACTED]` |
|
|
53
|
+
| `accessToken` | `[REDACTED]` |
|
|
54
|
+
| `githubAccessToken` | `[REDACTED]` |
|
|
55
|
+
| `secretKey` | `[REDACTED]` |
|
|
56
|
+
|
|
57
|
+
**2. Per-command field exclusion (`logExclude`):**
|
|
58
|
+
|
|
59
|
+
Commands can exclude additional fields via a static property. Excluded fields show `[excluded]` in logs:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export class CreateIntegrationCommand {
|
|
63
|
+
static logExclude = ['webhookSecret', 'apiKey'];
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
readonly name: string,
|
|
67
|
+
readonly webhookSecret: string,
|
|
68
|
+
readonly apiKey: string,
|
|
69
|
+
) {}
|
|
70
|
+
}
|
|
71
|
+
// Logs: { name: "Stripe", webhookSecret: "[excluded]", apiKey: "[excluded]" }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**3. Suppressing entire payload (`@Log({ logPayload: false })`):**
|
|
75
|
+
|
|
76
|
+
For commands where the entire payload is sensitive, disable payload logging — only metadata (name, duration, result status) is logged:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
@Log({ logPayload: false })
|
|
80
|
+
@Validate(BulkImportValidator)
|
|
81
|
+
export class BulkImportCommand {
|
|
82
|
+
constructor(readonly records: SensitiveRecord[]) {}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Payload Sanitization (automatic)
|
|
87
|
+
|
|
88
|
+
LogBehavior sanitizes all payloads before logging:
|
|
89
|
+
|
|
90
|
+
| Rule | Behavior |
|
|
91
|
+
|------|----------|
|
|
92
|
+
| Strings > 200 chars | Truncated: `"value..."` + `(N chars)` |
|
|
93
|
+
| Arrays > 5 items | First 5 items + `"... +N more"` |
|
|
94
|
+
| Object depth > 2 | Nested objects show `[nested]` |
|
|
95
|
+
|
|
96
|
+
## Pino HTTP Serializers
|
|
97
|
+
|
|
98
|
+
HTTP request/response logs are stripped to safe fields only:
|
|
99
|
+
- **Request:** `id`, `method`, `url`, `correlationId`
|
|
100
|
+
- **Response:** `statusCode`
|
|
101
|
+
|
|
102
|
+
No headers, bodies, or query parameters are logged at the HTTP layer.
|
|
103
|
+
|
|
104
|
+
## Handler Skip — Not Supported
|
|
105
|
+
|
|
106
|
+
There is no mechanism to skip logging for an entire handler. All commands/queries pass through `LogBehavior`. Use `@Log({ logPayload: false })` to suppress payload if needed.
|
|
107
|
+
|
|
108
|
+
## Key Metrics
|
|
109
|
+
|
|
110
|
+
- `http_request_duration_seconds`, `http_requests_total`
|
|
111
|
+
- `cqrs_command_duration_seconds`, `cqrs_command_errors_total`
|
|
112
|
+
- `redis_stream_lag`, `redis_stream_dlq_length`
|
|
113
|
+
- `typeorm_query_duration_seconds`
|
|
114
|
+
|
|
115
|
+
## Alerting Thresholds
|
|
116
|
+
|
|
117
|
+
| Alert | Condition | Severity |
|
|
118
|
+
|-------|-----------|----------|
|
|
119
|
+
| High API latency | p95 > 2s for 5 min | Warning |
|
|
120
|
+
| API error rate | 5xx > 5% for 5 min | Critical |
|
|
121
|
+
| Redis stream lag | Pending > 1000 for 10 min | Warning |
|
|
122
|
+
| DLQ growing | DLQ > 100 in 1 hour | Critical |
|
|
123
|
+
| Connection pool exhausted | Active > 90% pool | Critical |
|
|
124
|
+
| Pod OOM kill | Container OOMKilled restart | Critical |
|
|
125
|
+
|
|
126
|
+
## NEVER
|
|
127
|
+
|
|
128
|
+
- **NEVER** use `console.log` in application code — use Pino via the injected logger
|
|
129
|
+
- **NEVER** use unstructured log messages — all logs must be structured JSON key-value pairs
|
|
130
|
+
- **NEVER** log sensitive data (passwords, tokens, PII) — use `logExclude` or `@Log({ logPayload: false })`
|
|
131
|
+
- **NEVER** rely solely on built-in PII masking for domain-specific secrets — add them to `logExclude`
|
|
132
|
+
- **NEVER** leave critical paths without alerting
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: "client/src/**/*.{ts,tsx}"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Frontend Observability
|
|
6
|
+
|
|
7
|
+
## Sentry — Error Tracking + Performance
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
Sentry.init({
|
|
11
|
+
dsn: import.meta.env.VITE_SENTRY_DSN,
|
|
12
|
+
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration({ maskAllText: true })],
|
|
13
|
+
tracesSampleRate: import.meta.env.VITE_ENV === 'production' ? 0.1 : 1.0,
|
|
14
|
+
replaysOnErrorSampleRate: 1.0,
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Environment Configuration
|
|
19
|
+
|
|
20
|
+
| Environment | Error tracking | Performance sampling | Session replay |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| Local dev | Disabled | Disabled | Disabled |
|
|
23
|
+
| Dev / Staging | All errors | 100% | On error only |
|
|
24
|
+
| Production | All errors | 10% | On error only |
|
|
25
|
+
|
|
26
|
+
## Alert Triage
|
|
27
|
+
|
|
28
|
+
Unresolved Sentry issues must be triaged within 48 hours — assign an owner or mark as expected behavior.
|
|
29
|
+
|
|
30
|
+
## Web Vitals
|
|
31
|
+
|
|
32
|
+
LCP < 2.5s, INP < 200ms, CLS < 0.1. JS bundle < 200KB gzipped.
|
|
33
|
+
|
|
34
|
+
## Backend Correlation
|
|
35
|
+
|
|
36
|
+
Every API request includes `x-request-id` header for end-to-end tracing.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
api.interceptors.request.use((config) => {
|
|
40
|
+
config.headers['x-request-id'] = crypto.randomUUID();
|
|
41
|
+
return config;
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## NEVER
|
|
46
|
+
|
|
47
|
+
- **NEVER** use `console.log` in application code — use Sentry
|
|
48
|
+
- **NEVER** include monitoring tools (pino-pretty, debug transports) in production frontend bundles
|
|
49
|
+
- **NEVER** log PII to Sentry
|