create-warlock 4.2.6 → 4.2.8

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 (95) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/templates/warlock/.env.example +36 -0
  4. package/templates/warlock/.gitattributes +1 -0
  5. package/templates/warlock/.husky/pre-commit +4 -0
  6. package/templates/warlock/.prettierignore +4 -0
  7. package/templates/warlock/.prettierrc.json +10 -0
  8. package/templates/warlock/.vscode/settings.json +41 -0
  9. package/templates/warlock/README.md +57 -0
  10. package/templates/warlock/_.gitignore +6 -0
  11. package/templates/warlock/docs/new-module.md +551 -0
  12. package/templates/warlock/eslint.config.js +98 -0
  13. package/templates/warlock/package.json +74 -0
  14. package/templates/warlock/public/home.css +523 -0
  15. package/templates/warlock/skills/api-design/SKILL.md +461 -0
  16. package/templates/warlock/skills/code-standards/SKILL.md +595 -0
  17. package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
  18. package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
  19. package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
  20. package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
  21. package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
  22. package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
  23. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
  24. package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
  28. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
  29. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
  30. package/templates/warlock/src/app/auth/main.ts +9 -0
  31. package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
  32. package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
  33. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
  34. package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
  35. package/templates/warlock/src/app/auth/routes.ts +22 -0
  36. package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
  37. package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
  38. package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
  39. package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
  40. package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
  41. package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
  42. package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
  43. package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
  44. package/templates/warlock/src/app/auth/utils/types.ts +14 -0
  45. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
  46. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
  47. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
  48. package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
  49. package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
  50. package/templates/warlock/src/app/posts/routes.ts +8 -0
  51. package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
  52. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
  53. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
  54. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
  55. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
  56. package/templates/warlock/src/app/shared/routes.ts +4 -0
  57. package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
  58. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
  59. package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
  60. package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
  61. package/templates/warlock/src/app/shared/utils/router.ts +30 -0
  62. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
  63. package/templates/warlock/src/app/uploads/routes.ts +4 -0
  64. package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
  65. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
  66. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
  67. package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
  68. package/templates/warlock/src/app/users/events/sync.ts +5 -0
  69. package/templates/warlock/src/app/users/main.ts +5 -0
  70. package/templates/warlock/src/app/users/models/user/index.ts +1 -0
  71. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
  72. package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
  73. package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
  74. package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
  75. package/templates/warlock/src/app/users/routes.ts +8 -0
  76. package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
  77. package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
  78. package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
  79. package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
  80. package/templates/warlock/src/app/users/services/login-social.ts +19 -0
  81. package/templates/warlock/src/config/app.ts +12 -0
  82. package/templates/warlock/src/config/auth.ts +20 -0
  83. package/templates/warlock/src/config/cache.ts +59 -0
  84. package/templates/warlock/src/config/database.ts +65 -0
  85. package/templates/warlock/src/config/http.ts +23 -0
  86. package/templates/warlock/src/config/log.ts +22 -0
  87. package/templates/warlock/src/config/mail.ts +16 -0
  88. package/templates/warlock/src/config/repository.ts +11 -0
  89. package/templates/warlock/src/config/storage.ts +34 -0
  90. package/templates/warlock/src/config/tests.ts +5 -0
  91. package/templates/warlock/src/config/validation.ts +7 -0
  92. package/templates/warlock/storage/.gitignore +2 -0
  93. package/templates/warlock/tsconfig.json +27 -0
  94. package/templates/warlock/warlock.config.ts +15 -0
  95. package/templates/warlock/yarn.lock +2332 -0
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: module-boundaries
3
+ description: 'Architecture rules for how modules under `src/app/**` relate — each module owns one domain noun (`orders`, `users`, `carts`, `payments`); standard layout per module (`controllers/`, `services/`, `models/<noun>/`, `repositories/`, `schema/`, `resources/`, `types/`, `utils/`, `events/`, `errors/`, `routes.ts`); public surface exposed through sub-folder barrels (`services/index.ts`, `utils/index.ts`, `types/index.ts`) — NEVER a module-root `index.ts` re-exporting everything; no module reaches into another module''s internal files; zero circular dependencies between modules; inter-module communication is explicit — direct service call OR `@mongez/events` bus, never shared mutable globals; cross-cutting helpers live in `src/app/shared/`. Triggers: adding a new module under `src/app/**`; importing from another module; deciding where new code belongs; circular import error; ESLint `import/no-restricted-paths` violation; user asks "can module A use module B''s internals", "how should modules talk", "where should this code live", "I''m getting a circular dependency", "what folders does a module need", "what goes in shared". Skip: intra-module file organization (load `skills/code-standards/SKILL.md` § 4); barrel mechanics within a module (load `skills/code-standards/SKILL.md` § 2.2); deploying / packaging / monorepo questions; framework primitive details — load relevant `@warlock.js/*` skill.'
4
+ ---
5
+
6
+ # Module boundaries
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every module under `src/app/**` and every import that crosses between modules.
10
+
11
+ A module is a unit of substitution. If two modules are tangled, you can't refactor either without touching both. These rules keep modules independent enough that any one can be rewritten, extracted, or deleted with a contained blast radius.
12
+
13
+ > **Sub-agent rule:** Before adding a new module or importing across modules, read this file.
14
+
15
+ ---
16
+
17
+ ## 1. One domain noun per module
18
+
19
+ ### 1.1 The rule
20
+
21
+ A module is named after the noun it owns: `users`, `orders`, `carts`, `payments`, `ai-models`, `chats`. Lowercase, kebab-case, plural.
22
+
23
+ ### 1.2 If the name needs an "and", split it
24
+
25
+ `orders-and-payments/` is two modules. So is `users-and-auth/`. Each gets its own folder.
26
+
27
+ ### 1.3 Cross-cutting concerns live in `shared/`
28
+
29
+ Auth helpers, logging adapters, enum definitions, router utilities — anything used by more than one domain module — lives in `src/app/shared/`. The existing `src/app/shared/{enums.ts, get-random-color.ts, locales.ts, router.ts, ...}` is the pattern.
30
+
31
+ ### 1.4 Match modules to domain boundaries
32
+
33
+ If the business has an "orders team" and a "payments team", you probably have an `orders` module and a `payments` module — not one `commerce` module. Module boundaries should mirror who owns the code and who gets paged when it breaks.
34
+
35
+ ---
36
+
37
+ ## 2. Standard module layout
38
+
39
+ Every module follows this folder shape. Not every folder is required — only the ones the module actually uses.
40
+
41
+ ```
42
+ src/app/<module>/
43
+ controllers/ ← HTTP handlers (one per endpoint)
44
+ <verb>-<noun>.controller.ts
45
+ services/ ← business logic (one per use case)
46
+ index.ts ← sub-folder barrel ✅
47
+ <verb>-<noun>.service.ts
48
+ models/
49
+ <noun>/ ← model folder (Cascade convention)
50
+ <noun>.model.ts
51
+ index.ts
52
+ migrations/
53
+ <timestamp>-<noun>.migration.ts
54
+ repositories/ ← Cascade RepositoryManager subclasses
55
+ <nouns>.repository.ts
56
+ schema/ ← seal schemas (v.object + Infer<>), value + type per file
57
+ <verb>-<noun>.schema.ts
58
+ resources/ ← output mappers (defineResource)
59
+ <noun>.resource.ts
60
+ types/ ← TypeScript types and enums
61
+ index.ts ← sub-folder barrel ✅
62
+ <noun>-status.type.ts
63
+ utils/ ← pure helpers used inside the module
64
+ index.ts ← sub-folder barrel ✅
65
+ <verb>-<noun>.ts
66
+ events/ ← if the module emits events
67
+ <noun>-<verb>.event.ts
68
+ errors/ ← custom error classes
69
+ <noun>-not-found.error.ts
70
+ routes.ts ← the module's route registrations
71
+ README.md ← module purpose, public surface, ownership
72
+ ```
73
+
74
+ ### 2.1 Empty folders are fine to omit
75
+
76
+ Don't create `events/` if the module emits nothing. Don't create `errors/` until you have an error to put there. The layout is a vocabulary, not a checklist.
77
+
78
+ ### 2.2 No module-root `index.ts`
79
+
80
+ This is the one absolute rule:
81
+
82
+ ```
83
+ ❌ src/app/orders/index.ts ← banned
84
+ ✅ src/app/orders/services/index.ts ← good (sub-folder barrel)
85
+ ✅ src/app/orders/utils/index.ts ← good
86
+ ✅ src/app/orders/types/index.ts ← good
87
+ ```
88
+
89
+ See `skills/code-standards/SKILL.md` § 2.2 for the reasoning.
90
+
91
+ ---
92
+
93
+ ## 3. Public vs private surface
94
+
95
+ ### 3.1 Public (importable from other modules)
96
+
97
+ - `src/app/<module>/services` (the sub-folder barrel)
98
+ - `src/app/<module>/utils` (the sub-folder barrel)
99
+ - `src/app/<module>/types` (the sub-folder barrel)
100
+ - `src/app/<module>/resources/<noun>.resource.ts`
101
+ - `src/app/<module>/models/<noun>` (the model folder — for `BelongsTo("Name")` registrations and typed references)
102
+ - `src/app/<module>/errors/<noun>-<reason>.error.ts`
103
+
104
+ ### 3.2 Private (not importable from other modules)
105
+
106
+ - `src/app/<module>/controllers/*` — controllers serve HTTP; nobody else should call them
107
+ - `src/app/<module>/repositories/*` — repositories are a service-internal detail; other modules go through services
108
+ - `src/app/<module>/schema/*` — schemas belong to the endpoint that uses them
109
+ - `src/app/<module>/routes.ts` — registers routes for the framework, nobody imports it
110
+
111
+ ### 3.3 Enforce with ESLint
112
+
113
+ Add `eslint-plugin-import/no-restricted-paths` rules:
114
+
115
+ ```js
116
+ // eslint.config.js — proposed addition
117
+ {
118
+ rules: {
119
+ "import/no-restricted-paths": ["error", {
120
+ zones: [
121
+ { target: "src/app/*/controllers", from: "src/app/*/controllers", except: ["./"] },
122
+ { target: "src/app", from: "src/app/*/repositories", except: ["./*/services"] },
123
+ { target: "src/app", from: "src/app/*/schema", except: ["./*/controllers"] },
124
+ ],
125
+ }],
126
+ },
127
+ }
128
+ ```
129
+
130
+ The exact rule shape depends on the eslint config — but the principle is: cross-module imports go through services / utils / types / resources / models / errors, never through the private folders.
131
+
132
+ ---
133
+
134
+ ## 4. No reaching across module internals
135
+
136
+ ```typescript
137
+ // ✅ public surface — the module's barrel
138
+ import { listUsersService } from "app/users/services";
139
+
140
+ // ✅ public — a typed type
141
+ import { type User } from "app/users/types";
142
+
143
+ // ✅ public — model class for relations
144
+ import { User } from "app/users/models/user/user.model";
145
+
146
+ // ❌ private — repository is service-internal
147
+ import { usersRepository } from "app/users/repositories/users.repository";
148
+
149
+ // ❌ private — controller is HTTP-internal
150
+ import { listUsersController } from "app/users/controllers/list-users.controller";
151
+ ```
152
+
153
+ If you find yourself needing another module's internals, the boundary is wrong. Either:
154
+
155
+ - The internal API should be promoted to public (add a service that exposes it).
156
+ - The functionality belongs in `shared/`.
157
+ - The two modules should be merged.
158
+
159
+ ---
160
+
161
+ ## 5. Zero circular dependencies
162
+
163
+ ### 5.1 What a cycle looks like
164
+
165
+ `app/orders` imports `app/users/services`, and `app/users` imports `app/orders/services`. Even one level deep, this is a cycle.
166
+
167
+ ### 5.2 Why it's banned
168
+
169
+ - Runtime import-order bugs that only surface in production
170
+ - Refactor blast radius: changing either module forces changes in both
171
+ - Module-level reasoning breaks down — you can't think about either in isolation
172
+
173
+ ### 5.3 The fix
174
+
175
+ Almost always: extract the shared concept into a third module.
176
+
177
+ ```
178
+ ❌ orders ↔ users (cycle)
179
+ ✅ orders → users-shared ← shared types / events
180
+ users → users-shared
181
+ ```
182
+
183
+ Or, if the shared concept is purely cross-cutting (not a domain), it goes to `app/shared/`.
184
+
185
+ ### 5.4 Detect in CI
186
+
187
+ `eslint-plugin-import/no-cycle` catches cycles at lint time. Enable it.
188
+
189
+ ---
190
+
191
+ ## 6. Inter-module communication
192
+
193
+ ### 6.1 Direct call — when you need a result
194
+
195
+ `app/orders` needs the user's name to format an order confirmation: import `getUserService` from `app/users/services`, call it, use the result.
196
+
197
+ This is the default. Most inter-module communication is direct calls.
198
+
199
+ ### 6.2 Event bus — when you don't need a result
200
+
201
+ `app/orders` finishes placing an order; `app/notifications` wants to send a confirmation email; `app/analytics` wants to log the event. None of those need to block the order-placement flow, and orders shouldn't know about either.
202
+
203
+ Use `@mongez/events`:
204
+
205
+ ```typescript
206
+ // ✅ orders module emits
207
+ import { events } from "@mongez/events";
208
+ events.trigger("order.placed", { orderId, userId });
209
+
210
+ // ✅ notifications module listens
211
+ events.subscribe("order.placed", async ({ orderId, userId }) => {
212
+ await sendOrderConfirmationEmail(userId, orderId);
213
+ });
214
+ ```
215
+
216
+ ### 6.3 Pick rule
217
+
218
+ - "I need a result" → direct service call
219
+ - "Interested parties may want to know" → event
220
+ - "I need both" → direct call for the result + emit event for the others
221
+
222
+ ### 6.4 Never shared mutable globals
223
+
224
+ ```typescript
225
+ // ❌
226
+ // app/orders/state.ts
227
+ export let currentOrderCount = 0;
228
+
229
+ // app/dashboard/services/get-stats.service.ts
230
+ import { currentOrderCount } from "app/orders/state";
231
+ ```
232
+
233
+ Top-level mutable exports across modules are the worst kind of coupling — invisible, untyped, untestable. Always go through a service or an event.
234
+
235
+ ---
236
+
237
+ ## 7. Where shared code lives
238
+
239
+ | If the code is... | Put it in |
240
+ | --------------------------------------- | --------------------------------------------- |
241
+ | Used by 2+ modules, stable, cross-domain | `src/app/shared/` |
242
+ | Used by 2+ modules, domain-specific | Extract a new domain module |
243
+ | Used by exactly 1 module | Stays inside that module's `utils/` |
244
+ | Third-party integration glue (SDK wrappers) | `src/app/shared/` (or a dedicated module) |
245
+ | Build-time config | `src/config/` (one file per concern) |
246
+
247
+ ### 7.1 Promotion to `shared/` is a deliberate choice
248
+
249
+ If a util is duplicated across two modules, that's a signal — but the right answer isn't always "move to shared". Sometimes the two utils have drifted intentionally; sometimes one module is about to absorb the other. Promote only when the abstraction is genuinely stable.
250
+
251
+ ### 7.2 `shared/` is not a junkyard
252
+
253
+ Every file in `shared/` has a clear single purpose. If `shared/utils/` grows past ~20 files, split it by concern (`shared/strings/`, `shared/dates/`, etc.) before it becomes the codebase's attic.
254
+
255
+ ---
256
+
257
+ ## 8. Adding a new module — the checklist
258
+
259
+ When you create a new module:
260
+
261
+ 1. **Pick the noun.** Single domain concept. Lowercase, kebab-case, plural.
262
+ 2. **Create the folder.** `mkdir src/app/<noun>`.
263
+ 3. **Add `README.md`** at the module root. One paragraph: what the module owns, who owns the code, public surface.
264
+ 4. **Scaffold only what you need.** Don't pre-create `events/` and `errors/` if you won't use them today.
265
+ 5. **Add sub-folder barrels** only where you have 2+ files. A single service doesn't need a `services/index.ts` yet — but it will the moment a second service lands.
266
+ 6. **Wire `routes.ts`** for any HTTP endpoints, register events for any emitted signals.
267
+ 7. **Verify no module-root `index.ts`** was created.
268
+
269
+ ---
270
+
271
+ ## 9. Review checklist
272
+
273
+ Before merging a change that adds, restructures, or imports across modules:
274
+
275
+ - [ ] New module is named after a single domain noun
276
+ - [ ] Standard folder layout from § 2 (only the folders the module needs)
277
+ - [ ] No module-root `index.ts` created
278
+ - [ ] Sub-folder barrels exist for `services/`, `utils/`, `types/` where applicable
279
+ - [ ] No imports from another module's `controllers/`, `repositories/`, `schema/`
280
+ - [ ] No circular imports between modules (ESLint `no-cycle` clean)
281
+ - [ ] Cross-module communication uses direct service call or `@mongez/events` — never shared mutable globals
282
+ - [ ] Shared code that's used by 2+ modules lives in `src/app/shared/`, not duplicated
283
+ - [ ] Module has a `README.md` describing purpose, public surface, and ownership
@@ -0,0 +1,306 @@
1
+ ---
2
+ name: observability-and-resilience
3
+ description: 'How this project stays debuggable in production and survives flaky dependencies — `log.<level>(module, action, message, context)` from `@warlock.js/logger` with `X-Request-Id` propagation (already shipped via core middleware), structured metrics naming `<area>.<noun>.<unit>`, health endpoints (`/health/live` for liveness vs `/health/ready` for readiness), timeouts on every external call (`@warlock.js/http` `timeout` per Http instance / per request), retries with exponential backoff for idempotent ops only (never auto-retry POST without an idempotency key), idempotency keys for side effects (payments, emails, webhook deliveries), circuit breakers for flaky deps, graceful shutdown via `log.configure({ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"] })`, N+1 detection, streaming for large payloads. Triggers: making an HTTP / AI / queue / DB call; adding a timeout or retry; designing an idempotency key for a side effect; instrumenting metrics; adding a health check; tracing a request across services; user asks "timeout", "retry policy", "idempotency", "request id", "tracing", "graceful shutdown", "health check", "circuit breaker", "N+1 query", "caching strategy", "metrics naming"; touching `@warlock.js/http` / `@warlock.js/scheduler` / `@warlock.js/ai` call sites. Skip: log call signature (load `skills/code-standards/SKILL.md` § 8); secrets in logs (load `skills/security-baseline/SKILL.md` § 8); pure-function refactors with no external I/O; cache driver setup (load `@warlock.js/cache` skills); HTTP client config in depth (load `@warlock.js/http` skills).'
4
+ ---
5
+
6
+ # Observability & resilience
7
+
8
+ **Status:** Stable
9
+ **Applies to:** Every external call, every long-running operation, every production process.
10
+
11
+ Without observability you debug by guessing. Without resilience one slow dependency takes the whole service down. These rules cost a few extra lines per call site and save days of incident response.
12
+
13
+ > **Sub-agent rule:** Before making any external call or designing a long-running operation, read this file.
14
+
15
+ ---
16
+
17
+ ## 1. Request ID propagation
18
+
19
+ ### 1.1 One ID per request
20
+
21
+ The framework's `@warlock.js/core` HTTP middleware already attaches an `X-Request-Id` header — honoured if the client sent one, generated if not. The same id is echoed back in the response and available to every log call within the request's lifetime.
22
+
23
+ ### 1.2 Include it in every log line
24
+
25
+ Inside a request, pass the request id as part of the log context:
26
+
27
+ ```typescript
28
+ // ✅
29
+ log.info("orders", "order-placed", "Order created", {
30
+ orderId,
31
+ userId,
32
+ requestId: request.id,
33
+ });
34
+ ```
35
+
36
+ Without it, debugging a production incident is archaeology — you can't correlate the upstream log line with the downstream failure.
37
+
38
+ ### 1.3 Propagate downstream
39
+
40
+ When this service calls another service or a third-party API, forward the request id as `X-Request-Id` on the outgoing call. The full trace is what makes distributed debugging possible.
41
+
42
+ ---
43
+
44
+ ## 2. Timeouts
45
+
46
+ **Every external call has an explicit timeout.** No exceptions.
47
+
48
+ ### 2.1 HTTP — `@warlock.js/http`
49
+
50
+ Set a default at the `Http` instance level; override per request when the endpoint legitimately needs more.
51
+
52
+ ```typescript
53
+ // ✅ instance-level default
54
+ const stripeClient = new Http({
55
+ baseURL: "https://api.stripe.com",
56
+ timeout: 10_000, // 10s — enough for most calls, short enough to fail fast
57
+ });
58
+
59
+ // ✅ per-request override for a known-slow call
60
+ await stripeClient.post("/v1/payouts/preview", body, { timeout: 30_000 });
61
+ ```
62
+
63
+ ### 2.2 AI calls
64
+
65
+ AI calls run longer than typical HTTP. Use the per-provider defaults from `@warlock.js/ai` adapters and override per call when streaming a known-large completion. Never leave AI calls without a timeout — a hung model = a wedged worker.
66
+
67
+ ### 2.3 Database queries
68
+
69
+ Driver-level timeout (set in `config/database.ts`) + per-statement timeout for known-expensive queries. A 30-second query lock kills concurrency under load.
70
+
71
+ ### 2.4 Queue ops
72
+
73
+ Set the visibility timeout to match the expected processing time of the message — long enough that the handler finishes, short enough that a crashed worker doesn't park the message for hours.
74
+
75
+ ### 2.5 Why this matters
76
+
77
+ A dependency that hangs without timing out is worse than a dependency that errors. Errors are visible; hangs eat workers silently until the pool is empty and the service stops responding.
78
+
79
+ ---
80
+
81
+ ## 3. Retries
82
+
83
+ ### 3.1 Retry only idempotent ops
84
+
85
+ - **Safe to retry**: `GET`, `PUT`, `DELETE`, reads, lookups, queries
86
+ - **Never auto-retry**: `POST` without an idempotency key, queue ops without dedup, payment charges
87
+
88
+ A naive retry on a failed payment charge double-charges the customer. Don't.
89
+
90
+ ### 3.2 Exponential backoff with jitter
91
+
92
+ A retry storm against a recovering service brings it down again. Backoff with jitter spreads the load:
93
+
94
+ ```
95
+ attempt 1 → fail
96
+ wait 100ms ± 50ms
97
+ attempt 2 → fail
98
+ wait 250ms ± 100ms
99
+ attempt 3 → fail
100
+ wait 600ms ± 200ms
101
+ ```
102
+
103
+ ### 3.3 Bounded
104
+
105
+ Max retries: small (3–5). Past that, surface the failure to the caller and let them decide. Infinite-retry-with-backoff is just a slower hang.
106
+
107
+ ### 3.4 Retry budget
108
+
109
+ When a dependency is clearly down (sustained failure rate > 50% over 30s), stop retrying entirely and fast-fail. See § 5 circuit breakers.
110
+
111
+ ---
112
+
113
+ ## 4. Idempotency keys
114
+
115
+ ### 4.1 What needs one
116
+
117
+ Any operation whose accidental retry would cause real-world harm:
118
+
119
+ - Payment charges, refunds, payouts
120
+ - Outbound emails, SMS, push notifications
121
+ - Webhook deliveries
122
+ - Third-party POSTs that mutate state
123
+
124
+ ### 4.2 Key generation
125
+
126
+ - **Server-internal calls**: generate the key at the call site (UUID / nanoid), store it.
127
+ - **Client-originated mutations** (e.g. "place order" from a mobile app): the client generates the key and sends it as a header (`Idempotency-Key`); the server stores `key → result`.
128
+
129
+ ### 4.3 Storage
130
+
131
+ Persist `key → result` with a TTL longer than your retry window (24h is typical). On retry with the same key, return the stored result without re-executing.
132
+
133
+ ### 4.4 Pattern
134
+
135
+ ```typescript
136
+ // ✅
137
+ export async function chargePaymentService(input, idempotencyKey) {
138
+ const cached = await idempotencyRepository.find({ key: idempotencyKey });
139
+ if (cached) {
140
+ return cached.result;
141
+ }
142
+
143
+ const result = await stripeClient.post("/v1/charges", input);
144
+ await idempotencyRepository.create({ key: idempotencyKey, result, expiresAt: in24h });
145
+
146
+ return result;
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 5. Circuit breakers
153
+
154
+ ### 5.1 When to add one
155
+
156
+ For any dependency that's flaky enough that retries alone aren't enough — typically third-party APIs, edge services, anything outside your operational control.
157
+
158
+ ### 5.2 States
159
+
160
+ - **Closed** — normal operation, calls pass through.
161
+ - **Open** — failure rate exceeded threshold; calls fail fast without hitting the dependency.
162
+ - **Half-open** — after a cooldown, allow a few calls through to probe recovery.
163
+
164
+ ### 5.3 Fallback behaviour
165
+
166
+ When the breaker is open, the call site needs a plan:
167
+
168
+ - Return a cached value (if the data is cacheable)
169
+ - Queue for later (if the operation can wait)
170
+ - Fail-fast to the user (if neither — at least the user knows immediately)
171
+
172
+ Never silently swallow.
173
+
174
+ ---
175
+
176
+ ## 6. Health checks
177
+
178
+ ### 6.1 Two endpoints, two semantics
179
+
180
+ - **`/health/live`** — liveness. Is the process alive? Almost always returns 200; the only way it returns non-200 is if it's about to crash anyway. Used by the orchestrator to decide whether to kill and restart.
181
+ - **`/health/ready`** — readiness. Can the process serve traffic? Checks DB connectivity, queue connectivity, warm-up completion. Used by the load balancer to decide whether to route traffic.
182
+
183
+ ### 6.2 Don't conflate them
184
+
185
+ Returning 503 from `/health/live` because the DB is down causes the orchestrator to restart the process — which doesn't fix a DB problem and adds restart-loop chaos to an outage. Use `/health/ready` for "I can't serve right now" so the LB drains traffic without the orchestrator interfering.
186
+
187
+ ### 6.3 No internal details leaked
188
+
189
+ Health endpoints return a status code, not internal version info, env names, dependency hostnames, or stack traces. Those are information leaks under a different name.
190
+
191
+ ---
192
+
193
+ ## 7. Metrics
194
+
195
+ ### 7.1 Three primitive types
196
+
197
+ - **Counter** — things that happened, monotonically increasing (`orders.placed.total`, `auth.login.failed.total`)
198
+ - **Gauge** — current value (`queue.depth`, `connections.active`, `cache.size_bytes`)
199
+ - **Histogram** — distribution (`http.request.duration_ms`, `ai.completion.tokens`)
200
+
201
+ ### 7.2 Naming — `<area>.<noun>.<unit>`
202
+
203
+ Match the log `module` / `action` convention. The metric `orders.placed.total` corresponds to the log call `log.success("orders", "order-placed", ...)`. Symmetry across observability surfaces makes incidents debuggable.
204
+
205
+ ### 7.3 Cardinality discipline
206
+
207
+ **Never** include high-cardinality values as labels: user-id, request-id, order-id, email. Each unique value creates a new metric series; a million users = a million series = your monitoring backend's invoice goes vertical.
208
+
209
+ Use logs for high-cardinality data, metrics for aggregates.
210
+
211
+ ### 7.4 What to instrument
212
+
213
+ Start with the four golden signals:
214
+
215
+ - **Latency** — `http.request.duration_ms` histogram, by route
216
+ - **Traffic** — `http.requests.total` counter, by route + status
217
+ - **Errors** — `http.requests.errors.total` counter, by route + error class
218
+ - **Saturation** — queue depth, connection pool usage, memory
219
+
220
+ Add business metrics on top (`orders.placed.total`, `revenue.cents.total` — taking care with cardinality).
221
+
222
+ ---
223
+
224
+ ## 8. Graceful shutdown
225
+
226
+ ### 8.1 The sequence
227
+
228
+ SIGTERM (or SIGINT) →
229
+
230
+ 1. Stop accepting new requests / new queue messages (close the listeners).
231
+ 2. Finish in-flight work with a hard timeout (30s typical).
232
+ 3. Flush logs.
233
+ 4. Close DB / external connections cleanly.
234
+ 5. Exit 0.
235
+
236
+ ### 8.2 Logger flush
237
+
238
+ `@warlock.js/logger` covers step 3 when you configure it correctly:
239
+
240
+ ```typescript
241
+ // ✅ config/log.ts (or equivalent boot file)
242
+ log.configure({
243
+ channels: [/* ... */],
244
+ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"],
245
+ });
246
+ ```
247
+
248
+ Without `autoFlushOn`, buffered log entries are lost on shutdown — exactly when you most want them.
249
+
250
+ ### 8.3 The hard timeout matters
251
+
252
+ If in-flight work doesn't finish in 30s, kill it anyway. The alternative is hanging forever waiting for one stuck request, which makes deploys / restarts unreliable.
253
+
254
+ ---
255
+
256
+ ## 9. Performance defaults
257
+
258
+ These belong here because performance failures present as resilience failures — a slow query starves the pool, a missing index turns a 10ms call into a 5-second call, an unbounded list returns 50k rows and OOMs the response serializer.
259
+
260
+ ### 9.1 N+1 queries are bugs, not warnings
261
+
262
+ A list endpoint that fires one query per item is a bug. Either eager-load relations (Cascade's `.with("relation")`) or batch-fetch the related ids in a single query and join in-process.
263
+
264
+ A code review that lets an N+1 through is doing its job poorly. Add it to the review checklist (already in `skills/api-design/SKILL.md`).
265
+
266
+ ### 9.2 Pagination always
267
+
268
+ See `skills/api-design/SKILL.md` § 3. No unbounded list ever, even if today's dataset is 12 rows.
269
+
270
+ ### 9.3 Stream large payloads
271
+
272
+ For responses > 1MB or uploads > 1MB, stream end-to-end. Never buffer a 50MB file into memory to send it.
273
+
274
+ ### 9.4 Cache invalidation strategy
275
+
276
+ Pick one and stick to it per surface:
277
+
278
+ - **TTL** — simplest; tolerable staleness for a fixed window. Good for read-mostly catalog data.
279
+ - **Write-through** — invalidate on the corresponding write. Good for user-owned data where staleness is visible.
280
+ - **Event-driven** — invalidate on a published event. Good across multiple services.
281
+
282
+ `repository.listCached(...)` exists for hot read paths — use it where the dataset changes rarely. Invalidate on the corresponding write through the repository's hooks.
283
+
284
+ ### 9.5 No sync I/O on the request path
285
+
286
+ `readFileSync`, `execSync`, sync DB drivers — all banned in handler code. They block the event loop and crater throughput under load.
287
+
288
+ ---
289
+
290
+ ## 10. Review checklist
291
+
292
+ Before merging a change that touches an external call, long-running op, or production wiring:
293
+
294
+ - [ ] Request id propagated in every log line within the request lifecycle
295
+ - [ ] External call has an explicit timeout (HTTP, AI, queue, DB)
296
+ - [ ] Retries only on idempotent ops; non-idempotent ops have an idempotency key
297
+ - [ ] Idempotency keys persisted with a TTL longer than the retry window
298
+ - [ ] Circuit breaker considered for any flaky third-party dependency
299
+ - [ ] Health endpoints split — `/health/live` separate from `/health/ready`
300
+ - [ ] No high-cardinality values (user-id, request-id) as metric labels
301
+ - [ ] Metric names follow `<area>.<noun>.<unit>`, mirror log `module.action`
302
+ - [ ] `log.configure({ autoFlushOn: ["SIGINT", "SIGTERM", "beforeExit"] })` set
303
+ - [ ] No N+1 introduced — relations eager-loaded or batched
304
+ - [ ] No unbounded list endpoint
305
+ - [ ] No sync I/O on the request path
306
+ - [ ] Cache invalidation strategy declared for any new cache surface