@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,466 @@
1
+ ---
2
+ title: Conventions
3
+ description: How Zerotal auto-discovers and wires your app/* classes at boot, with no manual register() calls.
4
+ ---
5
+
6
+ # Conventions
7
+
8
+ Zerotal auto-discovers and wires your app classes by **convention** at boot — drop a file in the
9
+ right `app/*` directory and it just works, no manual `register()` calls. This covers providers,
10
+ middleware, services, models, observers, policies, event listeners, events, jobs, schedules, and
11
+ validators, plus optional auto-migration.
12
+
13
+ ## Mental model
14
+
15
+ Each kind of class lives in a well-known directory; a **concern descriptor** knows how to scan
16
+ that directory and register what it finds. Core owns a few concerns (events, services, listeners,
17
+ jobs, validators); packages contribute the rest (`models`/`observers` from ORM, `policies` from
18
+ auth, `schedules` from scheduler) via `app.registerConcern(...)`, so core depends on neither ORM
19
+ nor auth.
20
+
21
+ Discovery happens at three points in the boot sequence, each ordered so later steps can rely on
22
+ earlier ones:
23
+
24
+ ```text
25
+ # boot timeline (left → right)
26
+
27
+ app/providers/* → register → boot phases → app/middleware/* → convention phase
28
+ (before register, (your explicit + the (before routes (everything else,
29
+ full lifecycle) discovered providers) load) concerns by order)
30
+ ```
31
+
32
+ - **Providers** are discovered _before_ the register phase, so they run their full lifecycle.
33
+ - **Middleware** is discovered _before_ routes load, so routes can reference it by name.
34
+ - Everything else runs in the **convention phase**, after all providers have booted, with each
35
+ concern sorted by its `order` (lower runs first).
36
+
37
+ > **Note** — In development and tests Zerotal scans the filesystem. You can disable discovery or
38
+ > override any path via [config](#configuration); a generated manifest can replace runtime scanning
39
+ > in production.
40
+
41
+ ## Models — app/models/
42
+
43
+ Every `Model` subclass under `app/models/` is registered automatically:
44
+
45
+ ```typescript
46
+ // app/models/User.ts
47
+ import { Model, column, hasMany } from "@zerotal/orm";
48
+ import { Post } from "./Post.ts";
49
+
50
+ export class User extends Model {
51
+ @column() name!: string;
52
+ @column() email!: string;
53
+ @hasMany(() => Post, { foreignKey: "user_id" }) posts!: Post[];
54
+ }
55
+ ```
56
+
57
+ No `@table` needed. The table name is derived by convention — `pluralize(snake(ClassName))`:
58
+
59
+ | Class | Table |
60
+ | ---------- | ------------ |
61
+ | `User` | `users` |
62
+ | `BlogPost` | `blog_posts` |
63
+ | `Category` | `categories` |
64
+ | `Person` | `people` |
65
+
66
+ Override the name (or set timestamps/soft-deletes) with `@table` whenever you need to:
67
+
68
+ ```typescript
69
+ // app/models/Account.ts
70
+ @table("legacy_accounts", { softDeletes: true })
71
+ export class Account extends Model {
72
+ /* … */
73
+ }
74
+ ```
75
+
76
+ > **Warning** — Use real fields for convention models. Declare columns as `@column() name!: string`, not
77
+ > `@column() declare name: string`. The loader identifies a class's columns from a probe
78
+ > instance's own fields, and `declare` fields are erased at runtime. (With an explicit `@table`,
79
+ > either form works.)
80
+ >
81
+ > Models that ship **inside packages** (not under an app's `app/models/`) and models defined
82
+ > **inline in tests** are not auto-discovered — they keep using `@table`.
83
+
84
+ ## Observers — app/observers/
85
+
86
+ `XObserver` is attached to model `X` automatically (the `Observer` suffix is stripped and matched
87
+ against the discovered models):
88
+
89
+ ```typescript
90
+ // app/observers/UserObserver.ts
91
+ import type { ModelObserver } from "@zerotal/orm";
92
+ import type { User } from "../models/User.ts";
93
+
94
+ export class UserObserver implements ModelObserver<User> {
95
+ creating(user: User) {
96
+ user.uuid = crypto.randomUUID();
97
+ }
98
+ created(user: User) {
99
+ /* … */
100
+ }
101
+ }
102
+ ```
103
+
104
+ Override the target with `static model = SomeModel` when the name doesn't match.
105
+
106
+ ## Policies — app/policies/
107
+
108
+ `XPolicy` is registered with the Gate for model `X`:
109
+
110
+ ```typescript
111
+ // app/policies/PostPolicy.ts
112
+ import { Policy } from "@zerotal/auth";
113
+ import type { Post } from "../models/Post.ts";
114
+ import type { User } from "../models/User.ts";
115
+
116
+ export class PostPolicy extends Policy<Post> {
117
+ update(user: User, post: Post) {
118
+ return post.userId === user.id;
119
+ }
120
+ delete(user: User, post: Post) {
121
+ return post.userId === user.id;
122
+ }
123
+ }
124
+ ```
125
+
126
+ Authorization (`Gate.allows("update", post)`, `ctx.authorize(...)`) resolves the policy from the
127
+ model's class automatically. Override the target with `static model = Post`.
128
+
129
+ ## Events & listeners — app/events/, app/listeners/
130
+
131
+ A listener declares the event(s) it handles via `static listens`:
132
+
133
+ ```typescript
134
+ // app/events/UserRegistered.ts
135
+ export class UserRegistered {
136
+ constructor(public user: User) {}
137
+ }
138
+ ```
139
+
140
+ ```typescript
141
+ // app/listeners/SendWelcomeEmail.ts
142
+ import { UserRegistered } from "../events/UserRegistered.ts";
143
+
144
+ export class SendWelcomeEmail {
145
+ static listens = UserRegistered; // or an array: [UserRegistered, ...]
146
+ async handle(e: UserRegistered) {
147
+ await Mail.send(new WelcomeMail(e.user));
148
+ }
149
+ }
150
+ ```
151
+
152
+ The loader binds each listener on the app event bus — no `Emitter.on(...)` wiring. Classes under
153
+ `app/events/` need no registration of their own; importing them is the whole effect (it bundles
154
+ them and runs any module-level side effects before listeners bind).
155
+
156
+ ## Model events
157
+
158
+ A model can map its lifecycle events to event classes, which are dispatched on the bus when they
159
+ fire — listeners then react with no coupling to the model:
160
+
161
+ ```typescript
162
+ // app/models/Order.ts
163
+ export class Order extends Model {
164
+ static dispatchesEvents = { created: OrderPlaced, deleted: OrderCancelled };
165
+ }
166
+ ```
167
+
168
+ Keys are the lifecycle event names: `creating`, `created`, `updating`, `updated`, `saving`,
169
+ `saved`, `deleting`, `deleted`, `retrieved`. Each event class is constructed with the model
170
+ instance and emitted. Dispatch honours hook suppression, so factory seeding stays silent. If no
171
+ event bus is bound (ORM used standalone), it's a safe no-op.
172
+
173
+ > **Tip** — Observers and `dispatchesEvents` coexist. Use an **observer** to group all lifecycle
174
+ > handlers in one class, and **`dispatchesEvents`** for decoupled pub/sub with independent
175
+ > listeners.
176
+
177
+ ## Providers — app/providers/
178
+
179
+ Any `ServiceProvider` under `app/providers/` is registered automatically — you don't list it in
180
+ `bootstrap/providers.ts`. Discovered providers run the full lifecycle
181
+ (`onRegister` → `onBooting` → `onBooted`), appended **after** the providers you registered
182
+ explicitly (so framework providers boot first), and de-duplicated if also listed by hand.
183
+
184
+ ```typescript
185
+ // app/providers/AppServiceProvider.ts
186
+ import { ServiceProvider } from "zerotal";
187
+
188
+ export class AppServiceProvider extends ServiceProvider {
189
+ override onRegister() {
190
+ this.app.container.singleton("billing", () => new Billing());
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## Middleware — app/middleware/
196
+
197
+ Each middleware class under `app/middleware/` is registered as a **named group** under its class
198
+ name, so routes can reference it by string without importing it:
199
+
200
+ ```typescript
201
+ // app/middleware/EnsureSubscribed.ts
202
+ import { BaseMiddleware } from "zerotal";
203
+
204
+ export class EnsureSubscribed extends BaseMiddleware<{}> {
205
+ async handle(ctx, next) {
206
+ /* … */ return next(ctx);
207
+ }
208
+ }
209
+ ```
210
+
211
+ ```typescript
212
+ // routes/web.ts — reference by class name
213
+ Router.group({ middleware: ["EnsureSubscribed"] }, () => {
214
+ /* … */
215
+ });
216
+ ```
217
+
218
+ Middleware is **not** applied globally by default. Opt a class into the global pipeline with
219
+ `static global = true`:
220
+
221
+ ```typescript
222
+ // app/middleware/RequestId.ts
223
+ export class RequestId extends BaseMiddleware<{}> {
224
+ static global = true; // runs on every request
225
+ async handle(ctx, next) {
226
+ /* … */ return next(ctx);
227
+ }
228
+ }
229
+ ```
230
+
231
+ ## Services — app/services/
232
+
233
+ Service classes under `app/services/` auto-register with the container, so `App.make(MyService)`
234
+ resolves them with the lifetime they declare. A class opts into a lifetime with a `static lifetime`
235
+ flag:
236
+
237
+ ```typescript
238
+ // app/services/UsersService.ts
239
+ import { GateService } from "@zerotal/auth";
240
+
241
+ export class UsersService {
242
+ static lifetime = "singleton"; // "singleton" | "scoped" | "transient"
243
+ constructor(private gate: GateService) {}
244
+ }
245
+ ```
246
+
247
+ - `singleton` / `scoped` → bound with a factory that auto-wires a fresh instance via
248
+ `container.build()`.
249
+ - `transient` or no flag → nothing is registered; the container already auto-wires unregistered
250
+ classes on demand, so resolution still works (a new instance each time). Non-service exports
251
+ (types, helpers) are therefore ignored.
252
+
253
+ ## Jobs — app/jobs/
254
+
255
+ Queue job classes under `app/jobs/` are imported at boot, which triggers their
256
+ `JobRegistry.register()` self-registration — so dispatch works without a manual import or the
257
+ generated jobs barrel (the barrel is still used for production workers).
258
+
259
+ ```typescript
260
+ // app/jobs/NotifyFollowersJob.ts
261
+ import { Job } from "@zerotal/queue";
262
+
263
+ export class NotifyFollowersJob extends Job {
264
+ override readonly queue = "notifications";
265
+ override readonly maxAttempts = 5;
266
+
267
+ constructor(private readonly postId: number) {
268
+ super();
269
+ }
270
+
271
+ // Constructor arguments must be serialised to survive the queue.
272
+ override payload(): Record<string, unknown> {
273
+ return { postId: this.postId };
274
+ }
275
+
276
+ async handle(): Promise<void> {
277
+ // …
278
+ }
279
+ }
280
+ ```
281
+
282
+ Dispatch it from anywhere without importing the class into a barrel:
283
+
284
+ ```typescript
285
+ // in a controller
286
+ await Queue.dispatch(new NotifyFollowersJob(post.id));
287
+ ```
288
+
289
+ The registration key is the class name, so two jobs may not share one — see
290
+ [Queue](/docs/queue) for the full lifecycle.
291
+
292
+ ## Schedules — app/schedules/
293
+
294
+ Classes under `app/schedules/` that extend the `Schedule` base class are instantiated and
295
+ registered with the scheduler at boot. Each declares its cadence (a `cron` string or the fluent
296
+ `frequency()` method) and its work (`handle()`); the loader translates the class's declarative
297
+ settings into a scheduled task.
298
+
299
+ ```typescript
300
+ // app/schedules/SendDailyReports.ts
301
+ import { Schedule } from "@zerotal/scheduler";
302
+
303
+ export class SendDailyReports extends Schedule {
304
+ cron = "0 8 * * *";
305
+ withoutOverlapping = true;
306
+ async handle() {
307
+ await Queue.dispatch(new SendReportsJob());
308
+ }
309
+ }
310
+ ```
311
+
312
+ This concern runs in the **worker** environment (to execute tasks) and **console** (so
313
+ `schedule:list` can enumerate them); it never runs in `web`. Contributed by `SchedulerProvider`.
314
+ See [Scheduler](/docs/scheduler) for the full `Schedule` settings reference.
315
+
316
+ ## Validators — app/validators/
317
+
318
+ Files under `app/validators/` are auto-imported at boot, so any module-level registration they
319
+ perform is in place before the first request. This is where a shared `FormRequest` base class or a
320
+ rule set used across several requests belongs:
321
+
322
+ ```typescript
323
+ // app/validators/StorePostRequest.ts
324
+ import { FormRequest } from "@zerotal/validator";
325
+
326
+ export class StorePostRequest extends FormRequest {
327
+ rules(r) {
328
+ return {
329
+ title: r.string().min(3).max(120),
330
+ body: r.string().min(1),
331
+ };
332
+ }
333
+ }
334
+ ```
335
+
336
+ The auto-import matters for anything with a **side-effect at module scope**. A `FormRequest`
337
+ subclass imported by its controller would load anyway; a module that registers something on
338
+ import only runs because this concern imports it.
339
+
340
+ ## Auto-migration
341
+
342
+ Once models are registered at boot, Zerotal can sync the schema additively — create missing tables
343
+ and add missing columns to match your models (TypeORM-style `synchronize`):
344
+
345
+ ```typescript
346
+ // config/database.ts
347
+ import { DatabaseConfig } from "@zerotal/orm";
348
+
349
+ export default DatabaseConfig({
350
+ url: env("DATABASE_URL", "sqlite://./database.sqlite"),
351
+ synchronize: true, // dev convenience
352
+ });
353
+ ```
354
+
355
+ - **Opt-in:** off unless enabled (like TypeORM), and **hard-off in `production`** regardless —
356
+ there you run `migrate` with generated migration files. A common setup enables it only for
357
+ tests/local via env (`synchronize: env("APP_ENV") !== "production"`).
358
+ - **Additive by default:** `true` creates missing tables and adds missing columns; it never
359
+ drops or alters existing ones.
360
+ - **Disruptive opt-in:** pass `synchronize: { enabled: true, disruptive: true }` to also drop
361
+ columns no model declares anymore. This destroys their data (and logs a warning per drop), so
362
+ keep it to local/test. See [Migrations → Disruptive sync](/docs/migrations#disruptive-sync).
363
+
364
+ > **Danger** — Disruptive sync drops columns and the data they hold. Never enable it in production;
365
+ > the concern is hard-off there regardless, but keep it scoped to local/test even so.
366
+
367
+ ## Configuration
368
+
369
+ Auto-discovery is configured under the `conventions` key in `config/app.ts`. `AppConfig()` fills
370
+ in every default, so you only set what you want to change:
371
+
372
+ ```typescript
373
+ // config/app.ts
374
+ import { env } from "zerotal";
375
+ import { AppConfig } from "zerotal/config";
376
+
377
+ export default AppConfig({
378
+ name: "Example",
379
+ url: env("APP_URL", "http://localhost:3000"),
380
+ key: env("APP_KEY", "changeme-in-production"),
381
+
382
+ // Defaults shown — omit entirely unless you want to change them.
383
+ conventions: {
384
+ enabled: true,
385
+ paths: {
386
+ providers: "app/providers",
387
+ middleware: "app/middleware",
388
+ models: "app/models",
389
+ observers: "app/observers",
390
+ policies: "app/policies",
391
+ listeners: "app/listeners",
392
+ events: "app/events",
393
+ jobs: "app/jobs",
394
+ schedules: "app/schedules",
395
+ validators: "app/validators",
396
+ commands: "app/commands",
397
+ },
398
+ },
399
+ });
400
+ ```
401
+
402
+ | Field | Required | Default | Description |
403
+ | --------- | -------- | ---------------------- | ---------------------------------------------------------- |
404
+ | `enabled` | no | `true` | Master switch for convention-based auto-registration. |
405
+ | `paths` | no | the `app/*` dirs above | Per-concern directory overrides, relative to the app root. |
406
+
407
+ Set `conventions.enabled: false` to opt out entirely and wire everything manually. Path overrides
408
+ let you relocate any concern. Files starting with `_` and `*.test.ts` / `*.test.tsx` /
409
+ `*.spec.ts` / `*.d.ts` are always skipped.
410
+
411
+ ## Custom concerns
412
+
413
+ Discovery is extensible. A provider can contribute its own concern descriptor via
414
+ `app.registerConcern(...)`:
415
+
416
+ ```typescript
417
+ // app/providers/AppServiceProvider.ts — inside onRegister/onBooting
418
+ this.app.registerConcern({
419
+ name: "validators",
420
+ order: 60,
421
+ dir: "app/validators",
422
+ register(mod, ctx) {
423
+ for (const exported of Object.values(mod)) {
424
+ /* register exported as needed */
425
+ }
426
+ },
427
+ });
428
+ ```
429
+
430
+ `order` controls sequencing (lower runs first). A `run(ctx)` hook (no `dir`) defines a one-shot
431
+ step that runs after the scanned concerns at the same point in the ordering — this is how
432
+ auto-migration is wired (`order: 100`, `run`-only). Restrict a concern to certain runtimes with
433
+ `envs` (e.g. `envs: ["worker", "console"]`, as `schedules` does).
434
+
435
+ ## Reference
436
+
437
+ The concerns that run in the convention phase, in `order`. `events`, `services`, `listeners`,
438
+ `jobs`, and `validators` are owned by core; the rest are contributed by their packages.
439
+
440
+ | Concern | Directory | Order | Runs in | Contributed by |
441
+ | -------------- | ---------------- | ----- | --------------- | -------------------- |
442
+ | `events` | `app/events` | 5 | all | core |
443
+ | `models` | `app/models` | 10 | all | `@zerotal/orm` |
444
+ | `services` | `app/services` | 10 | all | core |
445
+ | `observers` | `app/observers` | 20 | all | `@zerotal/orm` |
446
+ | `policies` | `app/policies` | 30 | all | `@zerotal/auth` |
447
+ | `listeners` | `app/listeners` | 40 | all | core |
448
+ | `jobs` | `app/jobs` | 50 | all | core |
449
+ | `schedules` | `app/schedules` | 55 | worker, console | `@zerotal/scheduler` |
450
+ | `validators` | `app/validators` | 60 | all | core |
451
+ | `auto-migrate` | _(run-only)_ | 100 | non-production | `@zerotal/orm` |
452
+
453
+ `providers` (`app/providers`) and `middleware` (`app/middleware`) are not convention-phase concerns
454
+ — they are discovered earlier in the boot sequence (see [Mental model](#mental-model)).
455
+
456
+ `commands` (`app/commands`) is discovered by the CLI's command runner rather than the convention
457
+ phase — console, worker, and test environments only, since HTTP boot has no use for parsing CLI
458
+ files. It honours `conventions.enabled` and the `paths.commands` override like the rest. See
459
+ [Commands](/docs/commands#auto-discovery).
460
+
461
+ ## Next steps
462
+
463
+ - [Structure](/docs/structure) — see how the `app/*` directories fit the project layout.
464
+ - [Providers](/docs/providers) — register and boot services explicitly when convention isn't enough.
465
+ - [Config system](/docs/config-system) — how `config/*.ts` files are loaded and merged.
466
+ - [Migrations](/docs/migrations) — generate schema changes instead of relying on `synchronize`.
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: Cookies
3
+ description: Read and write raw HTTP cookies via the request/response headers when the session isn't the right fit.
4
+ ---
5
+
6
+ # Cookies
7
+
8
+ Most state you'd reach for cookies for is better handled by the
9
+ [session](/docs/session) — it stores data in a **signed, `HttpOnly`** cookie for you.
10
+ When you need to read or set a raw cookie directly, you work with the standard
11
+ `Request`/`Response` headers on the [HTTP context](/docs/context).
12
+
13
+ ## Which should I use?
14
+
15
+ - **Session** — the default. User state, flash messages, anything sensitive or
16
+ tamper-prone. The [session](/docs/session) driver signs and encrypts it for you.
17
+ - **Raw cookie** — small, non-sensitive client preferences the browser must read
18
+ too (a theme toggle, a dismissed-banner flag). Reach for the manual approach
19
+ below only here.
20
+
21
+ > **Danger** — A plain `Set-Cookie` value is fully client-visible and editable.
22
+ > Zerotal signs the session cookie so it can't be tampered with; for a tamper-proof
23
+ > value of your own, store it in the session or sign it yourself with
24
+ > [`Url.sign`](/docs/encryption#signed-urls) or [encryption](/docs/encryption) rather
25
+ > than trusting a raw cookie.
26
+
27
+ ## Reading a cookie
28
+
29
+ Cookies arrive in the request's `Cookie` header. Read and parse it from the context:
30
+
31
+ ```ts
32
+ // in a controller
33
+ import type { HttpContext } from "zerotal";
34
+
35
+ function readCookie(ctx: HttpContext, name: string): string | undefined {
36
+ const header = ctx.request.headers.get("Cookie") ?? "";
37
+ for (const part of header.split(";")) {
38
+ const [k, ...v] = part.trim().split("=");
39
+ if (k === name) return decodeURIComponent(v.join("="));
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ const theme = readCookie(ctx, "theme") ?? "light";
45
+ ```
46
+
47
+ ## Setting a cookie
48
+
49
+ Set the response with a `ctx` helper, then append a `Set-Cookie` header to it. Use
50
+ `append` (not `set`) so multiple cookies can be sent in one response:
51
+
52
+ ```ts
53
+ // in a controller
54
+ ctx.json({ ok: true }); // assigns ctx.response
55
+
56
+ ctx.response!.headers.append(
57
+ "Set-Cookie",
58
+ `theme=dark; Path=/; Max-Age=${60 * 60 * 24 * 365}; SameSite=Lax`,
59
+ );
60
+ ```
61
+
62
+ > **Note** — `ctx.json()` returns `void` — it sets `ctx.response` for you. Reach for
63
+ > the response object via `ctx.response` after calling a response helper; don't
64
+ > assign its return value.
65
+
66
+ ### Recommended attributes
67
+
68
+ | Attribute | Why |
69
+ | ------------------- | ------------------------------------------------------------------ |
70
+ | `Path=/` | Make the cookie apply site-wide. |
71
+ | `HttpOnly` | Hide it from JavaScript — use for anything sensitive. |
72
+ | `Secure` | Only send over HTTPS. Enable in production. |
73
+ | `SameSite=Lax` | Sensible CSRF-resistant default; use `Strict` for extra isolation. |
74
+ | `Max-Age=<seconds>` | Lifetime. Omit for a session cookie that clears on browser close. |
75
+
76
+ To **delete** a cookie, set it again with `Max-Age=0`:
77
+
78
+ ```ts
79
+ // in a controller
80
+ ctx.response!.headers.append("Set-Cookie", "theme=; Path=/; Max-Age=0");
81
+ ```
82
+
83
+ ## Framework-managed cookies
84
+
85
+ You rarely set cookies by hand — two parts of the framework manage their own:
86
+
87
+ - **Session cookie** — the [session](/docs/session) driver stores the whole session
88
+ in a single cookie, **signed with HMAC-SHA256** and flagged `HttpOnly`,
89
+ `SameSite=Lax` (and `Secure` in production). Its name (`cookie`) and lifetime
90
+ (`lifetime`) come from `config/session.ts`. Put user state in the session rather
91
+ than rolling your own signed cookie.
92
+ - **`XSRF-TOKEN` cookie** — [`CsrfMiddleware`](/docs/csrf) sets this readable
93
+ (non-`HttpOnly`) cookie after every request so Axios/Inertia can echo it back as
94
+ the `X-XSRF-TOKEN` header.
95
+
96
+ ## Next steps
97
+
98
+ - [Session](/docs/session) — signed, `HttpOnly` cookie-backed state (the usual choice).
99
+ - [CSRF Protection](/docs/csrf) — the `XSRF-TOKEN` cookie.
100
+ - [HTTP Context](/docs/context) — the request/response objects you read and write.
101
+ - [Encryption](/docs/encryption) — sign or encrypt your own cookie values.