@zerotal/arch 1.7.4 → 1.7.5

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 (119) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/docs/about.md +16 -16
  4. package/docs/admin/actions.md +9 -9
  5. package/docs/admin/auth.md +2 -2
  6. package/docs/admin/dashboard.md +5 -5
  7. package/docs/admin/extending-ui.md +12 -12
  8. package/docs/admin/extending.md +16 -7
  9. package/docs/admin/forms.md +4 -4
  10. package/docs/admin/index.md +2 -2
  11. package/docs/admin/operations.md +9 -9
  12. package/docs/admin/resources.md +3 -3
  13. package/docs/admin/structure.md +6 -6
  14. package/docs/admin/tables.md +8 -8
  15. package/docs/admin/testing.md +4 -4
  16. package/docs/ai.md +118 -11
  17. package/docs/application.md +12 -12
  18. package/docs/arch.md +25 -3
  19. package/docs/assets.md +11 -11
  20. package/docs/audit.md +13 -13
  21. package/docs/authentication.md +39 -39
  22. package/docs/authorization.md +13 -13
  23. package/docs/broadcasting/channels.md +2 -2
  24. package/docs/broadcasting/client.md +2 -2
  25. package/docs/broadcasting/events.md +9 -10
  26. package/docs/broadcasting/index.md +1 -1
  27. package/docs/broadcasting/testing.md +5 -5
  28. package/docs/cache.md +15 -13
  29. package/docs/carbon.md +35 -35
  30. package/docs/changelog.md +105 -1
  31. package/docs/client/index.md +13 -13
  32. package/docs/commands.md +4 -4
  33. package/docs/components.md +116 -115
  34. package/docs/config-system.md +4 -4
  35. package/docs/container.md +27 -27
  36. package/docs/context.md +27 -27
  37. package/docs/contributing.md +28 -0
  38. package/docs/controllers.md +10 -10
  39. package/docs/conventions.md +23 -15
  40. package/docs/cookies.md +3 -3
  41. package/docs/csrf.md +6 -6
  42. package/docs/database.md +22 -17
  43. package/docs/deployment.md +2 -2
  44. package/docs/devtools.md +16 -16
  45. package/docs/email-verification.md +8 -8
  46. package/docs/encryption.md +7 -7
  47. package/docs/errors.md +6 -6
  48. package/docs/events.md +12 -11
  49. package/docs/flow/components.md +32 -32
  50. package/docs/flow/decorators.md +18 -18
  51. package/docs/flow/events.md +16 -16
  52. package/docs/flow/forms.md +17 -17
  53. package/docs/flow/icons.md +5 -5
  54. package/docs/flow/index.md +20 -20
  55. package/docs/flow/layouts.md +35 -35
  56. package/docs/flow/lifecycle.md +16 -16
  57. package/docs/flow/models.md +9 -9
  58. package/docs/flow/pagination.md +8 -8
  59. package/docs/flow/performance.md +7 -7
  60. package/docs/flow/references.md +3 -3
  61. package/docs/flow/routing.md +26 -26
  62. package/docs/flow/testing.md +20 -20
  63. package/docs/getting-started.md +18 -13
  64. package/docs/health.md +17 -8
  65. package/docs/helpers.md +17 -17
  66. package/docs/i18n.md +10 -10
  67. package/docs/inertia/devtools.md +4 -4
  68. package/docs/inertia/index.md +6 -6
  69. package/docs/inertia/props.md +21 -21
  70. package/docs/inertia/rendering.md +9 -9
  71. package/docs/inertia/ssr.md +2 -2
  72. package/docs/lifecycle.md +2 -2
  73. package/docs/lock.md +11 -11
  74. package/docs/logger.md +12 -12
  75. package/docs/media.md +22 -22
  76. package/docs/middleware.md +17 -17
  77. package/docs/migrations.md +13 -13
  78. package/docs/monitor.md +6 -6
  79. package/docs/notifications.md +24 -24
  80. package/docs/orm/casts.md +14 -14
  81. package/docs/orm/factories.md +8 -8
  82. package/docs/orm/index.md +22 -20
  83. package/docs/orm/lifecycle.md +13 -13
  84. package/docs/orm/queries.md +28 -28
  85. package/docs/orm/relationships.md +16 -16
  86. package/docs/orm/serialization.md +11 -11
  87. package/docs/package-development.md +10 -10
  88. package/docs/pagination.md +14 -14
  89. package/docs/password-reset.md +6 -6
  90. package/docs/providers.md +11 -11
  91. package/docs/query-builder.md +18 -18
  92. package/docs/queue.md +14 -12
  93. package/docs/rate-limiting.md +6 -6
  94. package/docs/responses.md +14 -14
  95. package/docs/roles-and-2fa.md +9 -9
  96. package/docs/routing.md +44 -44
  97. package/docs/scaffolding.md +1 -1
  98. package/docs/scheduler.md +10 -10
  99. package/docs/seeding.md +6 -6
  100. package/docs/session.md +16 -14
  101. package/docs/social.md +14 -14
  102. package/docs/storage.md +25 -13
  103. package/docs/structure.md +2 -2
  104. package/docs/support-policy.md +23 -10
  105. package/docs/telemetry.md +12 -12
  106. package/docs/tenancy.md +34 -40
  107. package/docs/testing/browser.md +6 -6
  108. package/docs/testing/console.md +3 -3
  109. package/docs/testing/database.md +10 -10
  110. package/docs/testing/flow-browser.md +6 -6
  111. package/docs/testing/http.md +12 -12
  112. package/docs/testing/index.md +3 -3
  113. package/docs/testing/mocking.md +8 -8
  114. package/docs/upgrade.md +1 -1
  115. package/docs/validator.md +19 -19
  116. package/docs/view.md +14 -14
  117. package/package.json +4 -4
  118. package/src/index.ts +25 -1
  119. package/src/tools/searchDocs.ts +7 -0
@@ -11,7 +11,7 @@ Flow pages are plain TypeScript classes. Register them as routes with `Router.fl
11
11
 
12
12
  `Router.flow(path, PageClass, middleware?)` registers a `Component` subclass as a `GET` route. The third argument accepts an array of middleware classes that run on the **initial GET and on every WebSocket update** — this is Flow's persistent middleware model (no separate "attach middleware per WS frame" step required).
13
13
 
14
- ```typescript
14
+ ```typescript fragment
15
15
  // routes/web.ts
16
16
  import { Router } from "zerotal";
17
17
  import { DashboardPage } from "#app/flow/DashboardPage.tsx";
@@ -38,7 +38,7 @@ Router.flow("/admin", AdminPage, [RequireAuthMiddleware, RequireAdminMiddleware]
38
38
 
39
39
  Chain `.name()` to give a route a name for reverse URL generation:
40
40
 
41
- ```typescript
41
+ ```typescript fragment
42
42
  Router.flow("/dashboard", DashboardPage, [RequireAuthMiddleware]).name("dashboard");
43
43
  Router.flow("/posts", PostsPage).name("posts.index");
44
44
  Router.flow("/posts/:slug", PostDetailPage).name("posts.show");
@@ -48,7 +48,7 @@ Router.flow("/posts/:slug", PostDetailPage).name("posts.show");
48
48
 
49
49
  Use `Router.group()` to share a prefix and/or middleware across several Flow routes:
50
50
 
51
- ```typescript
51
+ ```typescript fragment
52
52
  Router.group({ prefix: "/app", middleware: [RequireAuthMiddleware] }, () => {
53
53
  Router.flow("/dashboard", DashboardPage); // /app/dashboard
54
54
  Router.flow("/profile", ProfilePage); // /app/profile
@@ -71,7 +71,7 @@ Middleware declared on the group is persistent: it re-runs on every WebSocket up
71
71
 
72
72
  When you call `.fileBasedRouting()` on `Application`, the framework scans the given directory and auto-registers any file that exports a `Component` subclass. No import required in a route file.
73
73
 
74
- ```typescript
74
+ ```typescript fragment
75
75
  // bootstrap/app.ts
76
76
  import { Application, basePath } from "zerotal";
77
77
  import providers from "./providers.ts";
@@ -114,7 +114,7 @@ app/flow/
114
114
 
115
115
  Export a `middleware` array from a file-route to attach middleware to that specific page:
116
116
 
117
- ```typescript
117
+ ```typescript fragment
118
118
  // app/flow/admin/DashboardPage.tsx
119
119
  import { RequireAuthMiddleware } from "#app/middleware/RequireAuth.ts";
120
120
  import { RequireAdminMiddleware } from "#app/middleware/RequireAdmin.ts";
@@ -128,7 +128,7 @@ export class DashboardPage extends Component {
128
128
 
129
129
  Or use a `_middleware.ts` file in a directory to apply middleware to every file in that directory:
130
130
 
131
- ```typescript
131
+ ```typescript fragment
132
132
  // app/flow/admin/_middleware.ts
133
133
  import { RequireAuthMiddleware } from "#app/middleware/RequireAuth.ts";
134
134
  import { RequireAdminMiddleware } from "#app/middleware/RequireAdmin.ts";
@@ -142,7 +142,7 @@ export const middleware = [RequireAuthMiddleware, RequireAdminMiddleware];
142
142
 
143
143
  For query string parameters (`/posts?page=2&search=typescript`), use the `@url` decorator. The field is filled from the URL on the first render, and stays in sync as the value changes:
144
144
 
145
- ```typescript
145
+ ```typescript fragment
146
146
  @url page: number = 1;
147
147
  @url search: string = "";
148
148
  @url status: string = "all";
@@ -158,7 +158,7 @@ For query string parameters (`/posts?page=2&search=typescript`), use the `@url`
158
158
 
159
159
  Where `@url` syncs a reactive prop **to** the URL, `this.currentUrl()` and `this.navigateCurrent()` go the other way — they **build** a URL from the one you're on with some query params changed, for filter links and instant filtering. Both are client-only helpers (the compiler rewrites them to the client runtime), so they update without a server round-trip to start.
160
160
 
161
- ```typescript
161
+ ```typescript fragment
162
162
  // Build a URL from the current one — returns a string, does NOT navigate.
163
163
  this.currentUrl({ query: { page: 3 } });
164
164
  // /posts?search=ts&page=2 → /posts?search=ts&page=3
@@ -175,7 +175,7 @@ The merge rules:
175
175
 
176
176
  **Use `currentUrl()` in a binding** — an attribute value or a text child. The compiler turns it into a reactive client binding, so the link always reflects the current URL:
177
177
 
178
- ```tsx
178
+ ```tsx fragment
179
179
  // Pagination links that preserve the active search / filters
180
180
  <a href={this.currentUrl({ query: { page: this.page - 1 } })} flow:navigate>Previous</a>
181
181
  <a href={this.currentUrl({ query: { page: this.page + 1 } })} flow:navigate>Next</a>
@@ -183,7 +183,7 @@ The merge rules:
183
183
 
184
184
  **Use `navigateCurrent()` in a handler** — perfect for instant filters that navigate as the user picks:
185
185
 
186
- ```tsx
186
+ ```tsx fragment
187
187
  <select onChange={(e) => this.navigateCurrent({ query: { status: e.target.value || null } })}>
188
188
  <option value="">All</option>
189
189
  <option value="active">Active</option>
@@ -199,7 +199,7 @@ down is the case where that's wrong — the user is looking at the control they
199
199
  just changed, and the results move out from under them. Pass `preserveScroll` to
200
200
  leave the viewport where it is:
201
201
 
202
- ```tsx
202
+ ```tsx fragment
203
203
  <select
204
204
  onChange={(e) =>
205
205
  this.navigateCurrent({ query: { status: e.target.value || null }, preserveScroll: true })
@@ -217,12 +217,12 @@ Pagination is the opposite case: page 2 should start at the top, so leave it off
217
217
 
218
218
  Name a field after the segment and it arrives filled. `/posts/:post` names the `Post` model, so the page receives the loaded record — no query, and no lookup code:
219
219
 
220
- ```typescript
220
+ ```typescript fragment
221
221
  // routes/web.ts — nothing to declare; :post is a Post
222
222
  Router.flow("/posts/:post", PostDetailPage);
223
223
  ```
224
224
 
225
- ```typescript
225
+ ```typescript fragment
226
226
  export class PostDetailPage extends Component {
227
227
  @locked post!: Post; // :post — the record, already loaded
228
228
 
@@ -252,7 +252,7 @@ Only `@locked` and `@expose` fields are filled, and only from segments the route
252
252
 
253
253
  When the field's name differs from the segment, `@param` says where it comes from — either the segment's name, or the model:
254
254
 
255
- ```typescript
255
+ ```typescript fragment
256
256
  export class PostDetailPage extends Component {
257
257
  @locked @param(Post) article!: Post; // whichever segment resolved to a Post
258
258
  @locked @param("post") alsoArticle!: Post; // or name the segment
@@ -266,7 +266,7 @@ Passing the model is the sturdier of the two — the field says what it wants an
266
266
 
267
267
  `onBoot()` and `onMount()` also receive the request itself — the same `HttpContext` a controller action gets — for anything the URL doesn't carry. It types `ctx.params` only, so the signed-in user is `{ user }`, not `params.user`:
268
268
 
269
- ```typescript
269
+ ```typescript fragment
270
270
  override async onMount({ user }: HttpContext) {
271
271
  this.canEdit = user?.id === this.post.authorId;
272
272
  }
@@ -278,14 +278,14 @@ The argument is optional, because a component can also be created outside a requ
278
278
 
279
279
  A URL segment fills the **page**. `/posts/:post` gives the page its `post`; the components inside it get nothing from the URL, even if one of their fields happens to share the segment's name. If a child needs the post, the page hands it over:
280
280
 
281
- ```tsx
281
+ ```tsx fragment
282
282
  // in the page's render()
283
283
  <PostCard post={this.post} compact />
284
284
  ```
285
285
 
286
286
  A prop lands on the field of the same name, before any hook runs:
287
287
 
288
- ```typescript
288
+ ```typescript fragment
289
289
  export class PostCard extends Component {
290
290
  @locked post!: Post; // required — no default
291
291
  @locked compact = false; // optional — false when the page omits it
@@ -309,7 +309,7 @@ Mark props `@locked` (or `@expose`) so their values survive round-trips in the c
309
309
 
310
310
  Coerce string params to numbers with `ctx.integer()`:
311
311
 
312
- ```typescript
312
+ ```typescript fragment
313
313
  override async onMount(ctx: HttpContext) {
314
314
  this.userId = ctx.integer("id") ?? 0;
315
315
  }
@@ -343,7 +343,7 @@ ctx.ip(); // string | null — client IP
343
343
 
344
344
  `@session` binds a field to a session key, so its value survives a browser refresh. Reads and writes go straight to the session; nothing is kept in the component's snapshot, so the browser never sees the value:
345
345
 
346
- ```typescript
346
+ ```typescript fragment
347
347
  @session preferredTheme: string = "light"; // the session's `preferredTheme`
348
348
  @session lastVisitedTab: string = "overview";
349
349
 
@@ -359,7 +359,7 @@ Requires `SessionMiddleware` on the route. See [Decorators](/docs/flow/decorator
359
359
 
360
360
  For values that aren't a field on this component — a cart, a flash bag, anything you set elsewhere — use the [`Session`](/docs/session) facade. It resolves the in-flight request's session, so it works in any hook or action:
361
361
 
362
- ```typescript
362
+ ```typescript fragment
363
363
  import { Session } from "@zerotal/session";
364
364
 
365
365
  override async onMount() {
@@ -381,7 +381,7 @@ Reach for `@session` when the value is a field on the component; reach for the f
381
381
 
382
382
  When `AuthMiddleware` (or your `RequireAuthMiddleware`) runs, it populates `ctx.user`. Access it via `request()`:
383
383
 
384
- ```typescript
384
+ ```typescript fragment
385
385
  import { request } from "zerotal";
386
386
 
387
387
  export class ProfilePage extends Component {
@@ -410,7 +410,7 @@ Because `onBoot()` runs on every request (initial GET and WebSocket), the auth c
410
410
 
411
411
  Declare static properties on the page class to control the document `<title>`, inject `<head>` content, and attach a layout:
412
412
 
413
- ```typescript
413
+ ```typescript fragment
414
414
  export class DashboardPage extends Component {
415
415
  // Sets <title>Dashboard</title> on the initial render
416
416
  static title = "Dashboard";
@@ -430,7 +430,7 @@ export class DashboardPage extends Component {
430
430
 
431
431
  `static title` takes a string, or a function of the component:
432
432
 
433
- ```typescript
433
+ ```typescript fragment
434
434
  static title = "Posts";
435
435
  static title = (c: PostPage) => `${c.post?.title ?? "Loading"} — My App`;
436
436
  ```
@@ -438,7 +438,7 @@ static title = (c: PostPage) => `${c.post?.title ?? "Loading"} — My App`;
438
438
  The function form is resolved on the server for every render and every patch, so a title
439
439
  that depends on state follows it without an action doing anything:
440
440
 
441
- ```typescript
441
+ ```typescript fragment
442
442
  @expose async loadPost(slug: string): Promise<void> {
443
443
  this.post = await Post.where("slug", slug).firstOrFail();
444
444
  // the title updates with it — nothing else to call
@@ -449,7 +449,7 @@ Only the resolved string is sent to the browser; the function stays on the serve
449
449
 
450
450
  For per-render `<head>` content (meta tags, OG tags), use `<Head>` inside `render()`:
451
451
 
452
- ```tsx
452
+ ```tsx fragment
453
453
  import { Head } from "@zerotal/flow";
454
454
 
455
455
  override async render() {
@@ -470,7 +470,7 @@ override async render() {
470
470
 
471
471
  A fully wired page with a dynamic route param, auth, session, query string, and layout:
472
472
 
473
- ```typescript
473
+ ```typescript fragment
474
474
  import { Component, expose, locked, url, session } from "@zerotal/flow";
475
475
  import { request } from "zerotal";
476
476
  import { RequireAuthMiddleware } from "#app/middleware/RequireAuth.ts";
@@ -13,7 +13,7 @@ Import it from `@zerotal/flow/testing`.
13
13
 
14
14
  `FlowTest.mount()` drives the **initial GET** cycle: `onBoot → onMount → onRendering → render → onRendered → onDehydrate`. It returns a test handle you can use for assertions and further interactions:
15
15
 
16
- ```typescript
16
+ ```typescript fragment
17
17
  import { FlowTest } from "@zerotal/flow/testing";
18
18
  import { CounterPage } from "#app/flow/CounterPage.tsx";
19
19
 
@@ -29,7 +29,7 @@ t.assertDontSee("Count: 1");
29
29
 
30
30
  Pass initial props to seed state before `onMount()` runs. Props are applied to the component instance before the lifecycle starts:
31
31
 
32
- ```typescript
32
+ ```typescript fragment
33
33
  const t = await FlowTest.mount(PostsPage, { page: 2, search: "TypeScript" });
34
34
 
35
35
  expect(t.page().search).toBe("TypeScript");
@@ -43,7 +43,7 @@ This is equivalent to the component being navigated to with `?page=2&search=Type
43
43
 
44
44
  `t.call(method, ...args)` drives a **WebSocket action frame**: `onBoot → onHydrate → [onUpdating/onUpdated] → action → onUpdate → onRendering → render → onRendered → onDehydrate`.
45
45
 
46
- ```typescript
46
+ ```typescript fragment
47
47
  await t.call("increment");
48
48
  expect(t.page().count).toBe(1);
49
49
  t.assertSee("Count: 1");
@@ -67,7 +67,7 @@ Any other error is passed to `onError()` — as in production — and then **ret
67
67
 
68
68
  When the error path is what you're testing, opt in with `tolerateErrors()`:
69
69
 
70
- ```typescript
70
+ ```typescript fragment
71
71
  const t = (await FlowTest.mount(CheckoutPage)).tolerateErrors();
72
72
  await t.call("submit");
73
73
 
@@ -79,7 +79,7 @@ t.assertFlashed("error", "Payment failed");
79
79
 
80
80
  Three methods change a property between calls:
81
81
 
82
- ```typescript
82
+ ```typescript fragment
83
83
  // set() — direct assignment, then re-renders; no hooks fire
84
84
  await t.set("draft", "Hello world");
85
85
  await t.set("page", 3);
@@ -98,7 +98,7 @@ await t.render(); // one render for both
98
98
 
99
99
  Use `set()` to put the component in a specific state for a test scenario. Use `update()` when you're testing that `onUpdating`/`onUpdated` hooks run correctly:
100
100
 
101
- ```typescript
101
+ ```typescript fragment
102
102
  // Test that onUpdatedUsername normalises to lowercase
103
103
  const t = await FlowTest.mount(ProfilePage);
104
104
  await t.update("username", "ALICE");
@@ -108,7 +108,7 @@ expect(t.page().username).toBe("alice"); // hook lowercased it
108
108
 
109
109
  ## HTML assertions
110
110
 
111
- ```typescript
111
+ ```typescript fragment
112
112
  t.assertSee("Published post"); // rendered HTML contains this string
113
113
  t.assertDontSee("Error"); // rendered HTML does NOT contain this string
114
114
  t.assertSee("<h1>Dashboard</h1>"); // can match HTML tags too
@@ -118,7 +118,7 @@ Both methods check `t.html()` — the raw rendered HTML string for this componen
118
118
 
119
119
  ## Validation assertions
120
120
 
121
- ```typescript
121
+ ```typescript fragment
122
122
  await t.call("register"); // trigger a validation action
123
123
 
124
124
  t.assertHasErrors("email"); // field has at least one error
@@ -131,7 +131,7 @@ t.assertNoErrors(); // no validation errors at all
131
131
 
132
132
  ## Redirect assertions
133
133
 
134
- ```typescript
134
+ ```typescript fragment
135
135
  await t.call("login");
136
136
 
137
137
  t.assertRedirectedTo("/dashboard"); // last action redirected to this URL
@@ -140,7 +140,7 @@ t.assertNotRedirected(); // last action did NOT redirect
140
140
 
141
141
  ## Flash assertions
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  await t.call("save");
145
145
 
146
146
  t.assertFlashed("success", "Saved."); // level + message substring
@@ -151,7 +151,7 @@ t.assertFlashed(); // any flash was emitted
151
151
 
152
152
  ## Event assertions
153
153
 
154
- ```typescript
154
+ ```typescript fragment
155
155
  await t.call("createPost");
156
156
 
157
157
  t.assertDispatched("post-created"); // event was dispatched
@@ -160,7 +160,7 @@ t.assertDispatched("post-created", { id: 1 }); // event was dispatched with this
160
160
 
161
161
  ## Accessors
162
162
 
163
- ```typescript
163
+ ```typescript fragment
164
164
  t.page(); // the Component instance — inspect properties and call methods directly
165
165
  t.html(); // the rendered HTML string from the last render
166
166
  t.errors(); // current error bag: Record<string, string[]>
@@ -170,7 +170,7 @@ t.snapshot(); // the serialised snapshot blob
170
170
 
171
171
  `t.page()` gives you the live component instance, so you can read any property:
172
172
 
173
- ```typescript
173
+ ```typescript fragment
174
174
  const page = t.page();
175
175
  expect(page.posts.length).toBe(10);
176
176
  expect(page.user?.email).toBe("alice@example.com");
@@ -191,7 +191,7 @@ That covers most actions on any page behind a sign-in, so open the scope yoursel
191
191
  rather than an object literal cast to the type — it carries a real `Request`, which matters as soon
192
192
  as anything downstream reads a header (an audited model records the actor's IP, for one):
193
193
 
194
- ```typescript
194
+ ```typescript fragment
195
195
  import { RequestContext, HttpContext } from "@zerotal/core";
196
196
 
197
197
  function asUser<T>(user: User | null, fn: () => Promise<T>): Promise<T> {
@@ -213,7 +213,7 @@ never makes a request to send it on.
213
213
 
214
214
  `FlowTest` does not set up or tear down a database — use your test suite's standard database helpers. With Bun, wrap tests in a transaction that rolls back after each test for full isolation:
215
215
 
216
- ```typescript
216
+ ```typescript fragment
217
217
  // tests/flow/PostsPage.test.ts
218
218
  import { describe, test, expect, beforeEach, afterEach } from "bun:test";
219
219
  import { FlowTest } from "@zerotal/flow/testing";
@@ -250,7 +250,7 @@ describe("PostsPage", () => {
250
250
 
251
251
  A complete example covering the common scenarios for a login page:
252
252
 
253
- ```typescript
253
+ ```typescript fragment
254
254
  // tests/flow/LoginPage.test.ts
255
255
  import { describe, test, expect } from "bun:test";
256
256
  import { FlowTest } from "@zerotal/flow/testing";
@@ -314,7 +314,7 @@ describe("LoginPage", () => {
314
314
 
315
315
  To test that a component dispatches events and that `@on` listeners respond, mount each component separately and verify the dispatch effect:
316
316
 
317
- ```typescript
317
+ ```typescript fragment
318
318
  test("dispatches post-created when saved", async () => {
319
319
  const editor = await FlowTest.mount(PostEditorPage);
320
320
  await t.set("title", "My post");
@@ -340,7 +340,7 @@ test("post list responds to post-created event", async () => {
340
340
 
341
341
  ## Testing redirects and navigation
342
342
 
343
- ```typescript
343
+ ```typescript fragment
344
344
  test("redirects to the created post after save", async () => {
345
345
  const t = await FlowTest.mount(NewPostPage);
346
346
  await t.set("title", "Hello");
@@ -387,7 +387,7 @@ A page can pass every `FlowTest` assertion and still do nothing when a person cl
387
387
  over the DevTools Protocol — no Puppeteer or Playwright dependency — so the click is a
388
388
  real click and the round-trip is a real round-trip.
389
389
 
390
- ```ts
390
+ ```ts fragment
391
391
  import { describe, it, expect, beforeAll, afterAll } from "bun:test";
392
392
  import { Application, Router } from "zerotal";
393
393
  import { FlowProvider } from "@zerotal/flow";
@@ -433,7 +433,7 @@ A few things are deliberate and worth copying:
433
433
  - **Guard the suite with `FlowBrowser.available()`** so it skips where no browser is
434
434
  installed instead of failing:
435
435
 
436
- ```ts
436
+ ```ts fragment
437
437
  const describeBrowser = FlowBrowser.available() ? describe : describe.skip;
438
438
  ```
439
439
 
@@ -12,7 +12,9 @@ provider.
12
12
  ## Requirements
13
13
 
14
14
  - **Bun** ≥ 1.3.14 — [install](https://bun.sh/docs/installation)
15
- - A PostgreSQL, MySQL, or SQLite database (SQLite requires nothing extra)
15
+ - A database, unless you scaffold the **Minimal** template. Every other template
16
+ is configured for **SQLite**, which needs nothing installed — PostgreSQL and
17
+ MySQL are supported and are a `DATABASE_URL` away.
16
18
 
17
19
  ## Create a new project
18
20
 
@@ -26,11 +28,11 @@ The scaffolder prompts for a project name and a template, then generates a
26
28
  ready-to-run project and installs dependencies. The **database** prompt only
27
29
  appears for the API template:
28
30
 
29
- | Prompt | Options |
30
- | ------------ | --------------------------------------------------------------------------- |
31
- | Project name | Defaults to `my-zerotal-app` |
32
- | Template | **API**, **Flow**, **React**, **Vue**, or **Minimal** (see the guide below) |
33
- | Database | **SQLite** (zero setup), **PostgreSQL**, or **MySQL** — API template only |
31
+ | Prompt | Options |
32
+ | ------------ | ---------------------------------------------------------------------------- |
33
+ | Project name | Defaults to `my-zerotal-app` |
34
+ | Template | **API**, **Admin**, **Flow**, **React**, **Vue**, or **Minimal** (see below) |
35
+ | Database | **SQLite** (zero setup), **PostgreSQL**, or **MySQL** — asked for **API** |
34
36
 
35
37
  It writes a fresh `APP_KEY` into the generated `.env.example` for you (no manual
36
38
  generation needed). For Postgres/MySQL it reminds you to set `DATABASE_URL`
@@ -47,7 +49,7 @@ bunx create-zerotal my-app --yes # take the defaults for anything uns
47
49
  bunx create-zerotal --help
48
50
  ```
49
51
 
50
- | Flag | |
52
+ | Flag | Values / description |
51
53
  | ------------------------- | -------------------------------------------------- |
52
54
  | `-t`, `--template <name>` | `api`, `admin`, `flow`, `react`, `vue`, `minimal` |
53
55
  | `--db <name>` | `sqlite`, `postgres`, `mysql` — API template only |
@@ -64,14 +66,17 @@ terminal: a half-built project that reports success is worse than one that stops
64
66
  ### Which template should I use?
65
67
 
66
68
  - **API** — JSON REST API with core, [ORM](/docs/orm), [auth](/docs/authentication),
67
- [validation](/docs/validator), and [testing](/docs/testing/index). The default for a backend service.
69
+ [validation](/docs/validator), and [testing](/docs/testing). The default for a backend service.
70
+ - **Admin** — an [admin panel](/docs/admin): resources, sign-in, dashboard widgets, and
71
+ seeded demo data. `bun zt db:seed` gives you an account to log in with.
68
72
  - **Flow** — server-driven reactive UI ([Flow](/docs/flow) pages, top nav, Tailwind).
69
73
  - **React** — [Inertia](/docs/inertia) + React SPA with file-based routes and Tailwind.
70
74
  - **Vue** — Inertia + Vue SPA with file-based routes and Tailwind.
71
75
  - **Minimal** — a single page with JSX views and Tailwind on the bare framework.
72
76
 
73
- > **Note** — Only the **API** template ships a database config and migrations.
74
- > The Flow, React, Vue, and Minimal templates start without a database.
77
+ > **Note** — every template except **Minimal** ships a database config and migrations,
78
+ > configured for SQLite. Only the **API** template _asks_ which database you want; the
79
+ > rest start on SQLite and switch when you set `DATABASE_URL`.
75
80
 
76
81
  ## Project structure
77
82
 
@@ -162,7 +167,7 @@ generated `package.json` also gives you `bun start` (production serve) and
162
167
 
163
168
  Open `routes/index.ts` and add:
164
169
 
165
- ```typescript
170
+ ```typescript fragment
166
171
  // routes/index.ts
167
172
  import { Router } from "zerotal";
168
173
  import { PostController } from "../app/controllers/PostController.ts";
@@ -239,7 +244,7 @@ bun zt migrate
239
244
 
240
245
  Your model is ready to use:
241
246
 
242
- ```typescript
247
+ ```typescript fragment
243
248
  // in a controller or service
244
249
  import { Post } from "../models/Post.ts";
245
250
 
@@ -257,7 +262,7 @@ Providers are where you wire up your own bindings. Scaffold one:
257
262
  bun zt make:provider AppServiceProvider
258
263
  ```
259
264
 
260
- ```typescript
265
+ ```typescript fragment
261
266
  // app/providers/AppServiceProvider.ts
262
267
  import { ServiceProvider } from "zerotal";
263
268
  import { PaymentGateway } from "../services/PaymentGateway.ts";
package/docs/health.md CHANGED
@@ -19,7 +19,7 @@ Health checks are built into `@zerotal/core` — nothing to install and no
19
19
  provider to register. The endpoint is registered when the application starts:
20
20
 
21
21
  ```typescript
22
- import { Health } from "zerotal";
22
+ import { Health } from "zerotal/health";
23
23
  ```
24
24
 
25
25
  ## Defaults
@@ -44,7 +44,11 @@ export default AppConfig({
44
44
  health: {
45
45
  enabled: true, // default: on outside production, off in production
46
46
  path: "/health", // default: '/health'
47
- secret: env("HEALTH_KEY"), // required in production
47
+ // Spread rather than assigned, because `secret?: string` under
48
+ // `exactOptionalPropertyTypes` (which the templates set) will not take the
49
+ // `undefined` that `env()` returns when the variable is unset. The framework
50
+ // reads the field the same way.
51
+ ...(env("HEALTH_KEY") ? { secret: env("HEALTH_KEY") } : {}), // required in production
48
52
  showDetails: true, // false → bare { "status": "ok" }
49
53
  },
50
54
  });
@@ -98,7 +102,7 @@ a built-in `runtime` probe (memory, Bun version, in-flight requests). It becomes
98
102
  a **readiness** probe as you register dependency checks against the `Health`
99
103
  registry exported from `zerotal`:
100
104
 
101
- ```typescript
105
+ ```typescript fragment
102
106
  // in a bootstrap file (e.g. bootstrap/health.ts)
103
107
  import { Health } from "zerotal/health";
104
108
 
@@ -113,7 +117,7 @@ Health.register(
113
117
 
114
118
  // Non-critical failures degrade the report without failing readiness.
115
119
  Health.register("cache", async () => {
116
- await Cache.put("__health", "1", 5);
120
+ await Cache.set("__health", "1", 5);
117
121
  return { meta: { driver: "redis" } };
118
122
  });
119
123
 
@@ -176,7 +180,7 @@ fails, since that is the case the endpoint exists for.
176
180
  ```typescript
177
181
  // tests/health/checks.test.ts
178
182
  import { test, expect, afterEach } from "bun:test";
179
- import { Health } from "zerotal";
183
+ import { Health } from "zerotal/health";
180
184
 
181
185
  afterEach(() => Health.clear());
182
186
 
@@ -189,7 +193,12 @@ test("a failing critical check brings the report down", async () => {
189
193
  { critical: true },
190
194
  );
191
195
 
192
- const report = await Health.run({ name: "app", version: "1.0.0", environment: "test" });
196
+ const report = await Health.run({
197
+ name: "app",
198
+ version: "1.0.0",
199
+ environment: "test",
200
+ uptime: 0, // required — seconds since boot, and a test has none worth reporting
201
+ });
193
202
 
194
203
  expect(report.status).toBe("down");
195
204
  expect(report.checks.database?.status).toBe("down");
@@ -205,7 +214,7 @@ to do with it.
205
214
  design. A non-critical check that fails must leave the overall status up,
206
215
  otherwise a flaky cache probe takes your deployment out of the load balancer:
207
216
 
208
- ```typescript
217
+ ```typescript fragment
209
218
  // tests/health/checks.test.ts
210
219
  test("a non-critical failure degrades without going down", async () => {
211
220
  Health.register("cache", async () => {
@@ -222,7 +231,7 @@ test("a non-critical failure degrades without going down", async () => {
222
231
  **The endpoint's access rules deserve their own test**, because a health endpoint
223
232
  that leaks internals is a reconnaissance gift:
224
233
 
225
- ```typescript
234
+ ```typescript fragment
226
235
  // tests/http/health.test.ts
227
236
  const res = await app.get("/health");
228
237