@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
package/docs/about.md ADDED
@@ -0,0 +1,676 @@
1
+ ---
2
+ title: About Zerotal
3
+ description: The complete standalone tour of Zerotal — what it is, the ideas behind it, and runnable examples for every major feature. Everything you need to start building on one page.
4
+ ---
5
+
6
+ # About Zerotal
7
+
8
+ Zerotal is a **Bun-native, full-stack TypeScript web framework** for building
9
+ full-stack apps — from classic server-rendered pages to reactive live components and
10
+ Inertia.js SPAs.
11
+
12
+ This page is the **whole framework on one screen**: read it top to bottom and you'll
13
+ know what Zerotal is, how it's put together, and how to do the everyday things —
14
+ routing, models, validation, auth, background jobs, and picking a frontend — with
15
+ copy-pasteable code. Every section links to the deep-dive doc if you want more, but
16
+ you shouldn't _need_ to open them to get productive. When you're ready to scaffold,
17
+ skip to [Install and run](#install-and-run).
18
+
19
+ ## The one-paragraph version
20
+
21
+ You write plain TypeScript classes — controllers, models, jobs, providers — and drop
22
+ them into convention-named folders (`app/controllers/`, `app/models/`, …). At boot,
23
+ Zerotal discovers them, wires their dependencies through a typed IoC container, and
24
+ runs everything on [Bun](https://bun.sh). There is **no build step**: Bun executes
25
+ `.ts` files directly, so what you write is what runs. If you've used a classic
26
+ full-stack MVC framework, the mental model transfers almost one-to-one — facades,
27
+ service providers, Active Record models, FormRequests, queues, policies — but the
28
+ language is TypeScript and the runtime is Bun.
29
+
30
+ ## Who Zerotal is for
31
+
32
+ - **Full-stack MVC developers** who want the same ergonomics with end-to-end type safety.
33
+ - **TypeScript teams** who want a batteries-included backend without stitching a
34
+ dozen libraries together.
35
+ - **Full-stack builders** who want to pick their frontend style per project —
36
+ server-rendered JSX, reactive server components, or a React/Vue SPA — without
37
+ changing frameworks.
38
+
39
+ ## Four ideas hold it together
40
+
41
+ Everything else is detail. Internalize these four and the rest reads as variations on
42
+ a theme.
43
+
44
+ ### 1. Bun-native, source-only
45
+
46
+ Zerotal runs **only on Bun** (≥ 1.1) and leans on Bun's APIs throughout — `Bun.sql`
47
+ for the database, `Bun.CryptoHasher` for hashing, `Bun.build` for bundling. Because
48
+ Bun runs and type-strips TypeScript natively, packages ship as **`.ts` source with no
49
+ compiled `dist/`**. You always read real source and get accurate types; there's
50
+ nothing to compile and no source-map indirection.
51
+
52
+ > **Note** — Node.js is not supported. Importing `@zerotal/*` from a plain Node
53
+ > process will fail — it expects Bun's runtime APIs.
54
+
55
+ ### 2. Convention over configuration
56
+
57
+ Drop a file in the right folder and it's wired up. Models in `app/models/`, providers
58
+ in `app/providers/`, policies, observers, and event listeners are all
59
+ **auto-discovered at boot** — no manual registration. Table names, route model
60
+ bindings, and more are derived by convention (`Post` → the `posts` table). You edit
61
+ `bootstrap/app.ts` only when you need to control ordering.
62
+
63
+ ### 3. The container wires everything
64
+
65
+ A typed **IoC (inversion-of-control) container** builds your objects for you. You
66
+ declare _how_ to build something once (usually in a provider), then ask for it by type
67
+ and the container resolves its dependencies. Controllers, services, and framework
68
+ internals are all resolved this way, which is what makes them easy to swap and test.
69
+
70
+ ### 4. Providers are the on-switches
71
+
72
+ A **service provider** is a feature's setup script. Registering a provider in
73
+ `bootstrap/providers.ts` is what "turns on" a package — it binds services into the
74
+ container, registers middleware, subscribes to events, and cleans up on shutdown, each
75
+ in a defined [lifecycle](/docs/lifecycle) phase (`onRegister` → `onBooting` →
76
+ `onBooted` → `onStopping`). Want Inertia? Add `InertiaProvider`. Want reactive SSR?
77
+ Add `FlowProvider`. The provider list is the feature manifest for your app.
78
+
79
+ ## Install and run
80
+
81
+ ```bash
82
+ # in your project's parent directory
83
+ bun create zerotal my-app # choose: API, Flow, React, Vue, or Minimal
84
+ cd my-app
85
+ cp .env.example .env # APP_KEY is pre-generated for you
86
+ bun zt migrate # create the database schema (API template)
87
+ bun run dev # dev server + hot reload at http://localhost:3000
88
+ ```
89
+
90
+ The scaffolder writes a fresh `APP_KEY` into `.env.example` and installs
91
+ dependencies. Pick a template at the prompt:
92
+
93
+ - **API** — JSON REST API (core + ORM + auth + validation + testing). The default.
94
+ - **Flow** — server-driven reactive UI (see [Flow](#flow-reactive-ssr)).
95
+ - **React** / **Vue** — Inertia SPA (see [Inertia](#inertia-react-vue-spa)).
96
+ - **Minimal** — one page with JSX views on the bare framework.
97
+
98
+ There's **no build step**: `bun run dev` starts the server, `bun test` runs the suite,
99
+ `bun run typecheck` type-checks. More in [Getting Started](/docs/getting-started).
100
+
101
+ ## The shape of an app
102
+
103
+ The folders are conventions the framework reads, not wiring you maintain:
104
+
105
+ ```
106
+ # my-app/
107
+ my-app/
108
+ ├── app/
109
+ │ ├── controllers/ # HTTP controllers (plain classes)
110
+ │ ├── models/ # ORM models — auto-discovered
111
+ │ ├── middleware/ # Custom middleware
112
+ │ ├── policies/ # Authorization policies
113
+ │ ├── providers/ # Your service providers — auto-discovered
114
+ │ ├── jobs/ # Queue jobs
115
+ │ ├── mail/ # Mailable classes
116
+ │ └── flow/ # Reactive SSR components (*.tsx)
117
+ ├── bootstrap/
118
+ │ ├── app.ts # Builds the Application (providers + routing)
119
+ │ └── providers.ts # Which packages are active
120
+ ├── config/ # Typed config files — auto-loaded
121
+ ├── database/
122
+ │ └── migrations/ # Schema-builder migration classes
123
+ ├── routes/
124
+ │ └── index.ts # Route definitions
125
+ ├── public/ # Static assets
126
+ ├── storage/ # Uploads, logs, SQLite file
127
+ ├── zt.ts # The CLI entry point — do not edit
128
+ └── .env
129
+ ```
130
+
131
+ `zt.ts` is the entry point for **everything** — `bun zt serve`, `migrate`, `test`,
132
+ and every `make:*` generator run through it. It boots `bootstrap/app.ts`:
133
+
134
+ ```ts
135
+ // bootstrap/app.ts
136
+ import { Application } from "zerotal";
137
+ import { Handler } from "../app/exceptions/Handler.ts";
138
+ import { User } from "../app/models/User.ts";
139
+ import providers from "./providers.ts";
140
+
141
+ export default Application.create({ providers })
142
+ .withExceptionHandler(Handler)
143
+ .withUserResolver((id) => User.find(id));
144
+ ```
145
+
146
+ See [Directory Structure](/docs/structure) for the full tour.
147
+
148
+ ## How a request flows
149
+
150
+ Most of what you write plugs into one of these steps:
151
+
152
+ 1. **Boot** — `bun zt serve` loads `bootstrap/app.ts`, registers providers,
153
+ auto-discovers `app/` and `config/`, and loads `routes/`.
154
+ 2. **Match** — the request is matched to a route → a controller action or a closure.
155
+ 3. **Pipeline** — it passes through the [middleware](/docs/middleware) stack (session,
156
+ auth, CSRF, …) before reaching your handler.
157
+ 4. **Handle** — your controller runs. It receives an [`HttpContext`](/docs/context)
158
+ for request input and the response, and resolves dependencies from the container.
159
+ 5. **Respond** — you return JSON, a [view](/docs/view), a [Flow](#flow-reactive-ssr)
160
+ page, an [Inertia](#inertia-react-vue-spa) page, or a redirect.
161
+
162
+ Read [Request Lifecycle](/docs/lifecycle) for the exact sequence.
163
+
164
+ ## Routing
165
+
166
+ Routes are registered by calling static `Router` methods at module load — map a path
167
+ to a controller + action, or to an inline closure:
168
+
169
+ ```ts
170
+ // routes/index.ts
171
+ import { Router, view, type HttpContext } from "zerotal";
172
+ import HomeController from "../app/controllers/HomeController.ts";
173
+ import PostController from "../app/controllers/PostController.ts";
174
+ import AdminController from "../app/controllers/AdminController.ts";
175
+ import { CounterPage } from "../app/flow/CounterPage.tsx";
176
+
177
+ // Controller + action
178
+ Router.get("/", HomeController, "index").name("home");
179
+
180
+ // Inline closure handler
181
+ Router.get("/health", (http: HttpContext) => http.json({ ok: true }));
182
+
183
+ // RESTful resource (index/create/store/show/edit/update/destroy)
184
+ Router.resource("posts", PostController);
185
+
186
+ // Reactive SSR page (Flow)
187
+ Router.flow("/counter", CounterPage);
188
+
189
+ // Route groups with shared prefix + middleware
190
+ Router.group({ prefix: "/admin", middleware: ["auth", "admin"] }, () => {
191
+ Router.get("/dashboard", AdminController, "index");
192
+ });
193
+ ```
194
+
195
+ Apps that prefer file-based routing add `.fileBasedRouting(basePath("app/pages"))` to
196
+ the app builder. Full details in [Routing](/docs/routing).
197
+
198
+ ## Controllers
199
+
200
+ Controllers are plain classes; the action receives the request `HttpContext` and
201
+ dependencies resolve from the container:
202
+
203
+ ```ts
204
+ // app/controllers/PostController.ts
205
+ import type { HttpContext } from "zerotal";
206
+ import Post from "../models/Post.ts";
207
+ import { StorePostRequest } from "../requests/StorePostRequest.ts";
208
+
209
+ export default class PostController {
210
+ async index(ctx: HttpContext) {
211
+ const posts = await Post.query()
212
+ .where("published", true)
213
+ .with(["author", "tags"])
214
+ .orderBy("created_at", "desc")
215
+ .paginate(1, 20); // (page, perPage)
216
+ return ctx.json(posts);
217
+ }
218
+
219
+ async store(ctx: HttpContext) {
220
+ const data = await StorePostRequest.validate(); // reads the current HttpContext
221
+ const post = await Post.create(data);
222
+ return ctx.json(post, 201);
223
+ }
224
+ }
225
+ ```
226
+
227
+ Read route params and model bindings from `ctx.params`; type them with the generic,
228
+ e.g. `ctx: HttpContext<{ post: Post }>`. More in [Controllers](/docs/controllers) and
229
+ [Context](/docs/context).
230
+
231
+ ## Models, queries, and migrations
232
+
233
+ Active Record–style models backed by `Bun.sql`. Columns and relationships are
234
+ decorators; the table name is derived by convention (so `@table` is optional):
235
+
236
+ ```ts
237
+ // app/models/Post.ts
238
+ import { Model, table, column, hasMany, belongsTo } from "@zerotal/orm";
239
+
240
+ @table("posts")
241
+ export default class Post extends Model {
242
+ @column() declare id: number;
243
+ @column() declare title: string;
244
+ @column("text") declare body: string;
245
+ @column("boolean") declare published: boolean;
246
+ @column("datetime") declare createdAt: Date;
247
+
248
+ @hasMany(() => Comment) declare comments: Comment[];
249
+ @belongsTo(() => User) declare author: User;
250
+
251
+ // Reusable query scope
252
+ static published = Model.scope((q) => q.where("published", true));
253
+ }
254
+ ```
255
+
256
+ The fluent query builder and relationship loading:
257
+
258
+ ```ts
259
+ // in a controller or service
260
+ const posts = await Post.query()
261
+ .withScopes((s) => s.published())
262
+ .with(["author", "comments"])
263
+ .orderBy("created_at", "desc")
264
+ .limit(20)
265
+ .get();
266
+
267
+ const post = await Post.find(1);
268
+ await post.load(["comments"]);
269
+
270
+ const created = await Post.create({ title: "Hello", body: "..." });
271
+ created.fill({ published: true });
272
+ await created.save();
273
+ ```
274
+
275
+ Schema changes are TypeScript migration classes under `database/migrations/`, run with
276
+ `bun zt migrate`:
277
+
278
+ ```ts
279
+ // database/migrations/001_create_posts_table.ts
280
+ import { Migration, Schema } from "@zerotal/orm";
281
+
282
+ export default class CreatePostsTable extends Migration {
283
+ async up() {
284
+ await Schema.create("posts", (table) => {
285
+ table.increments("id");
286
+ table.integer("user_id").index();
287
+ table.string("title");
288
+ table.text("body");
289
+ table.boolean("published").default(false);
290
+ table.timestamps();
291
+ });
292
+ }
293
+
294
+ async down() {
295
+ await Schema.drop("posts");
296
+ }
297
+ }
298
+ ```
299
+
300
+ Go deeper: [ORM](/docs/orm), [Query Builder](/docs/query-builder),
301
+ [Relationships](/docs/orm/relationships), [Migrations](/docs/migrations).
302
+
303
+ ## Validation
304
+
305
+ Validation is class-based via `FormRequest` with a fluent `RuleBuilder`, so a
306
+ controller trusts its data by the time it runs:
307
+
308
+ ```ts
309
+ // app/requests/StorePostRequest.ts
310
+ import { FormRequest, type RuleBuilder } from "@zerotal/validator";
311
+
312
+ export class StorePostRequest extends FormRequest {
313
+ // Don't annotate the return type — it's inferred, which types the result of validate()
314
+ rules(r: RuleBuilder) {
315
+ return {
316
+ title: r.string().min(3).max(255),
317
+ body: r.string().min(10),
318
+ tags: r.array(r.string()).optional(),
319
+ };
320
+ }
321
+ }
322
+ ```
323
+
324
+ ```ts
325
+ // in a controller — reads the current HttpContext, returns typed data,
326
+ // throws a 422 (JSON) or a redirect-back on failure.
327
+ const data = await StorePostRequest.validate();
328
+ ```
329
+
330
+ More rules and patterns in [Validation](/docs/validator).
331
+
332
+ ## Authentication and authorization
333
+
334
+ Session auth, bearer tokens, and policy-based authorization ship together:
335
+
336
+ ```ts
337
+ // in a controller / service
338
+ import { Auth, Gate, createToken } from "@zerotal/auth";
339
+
340
+ // Login
341
+ await Auth.attempt({ email, password });
342
+
343
+ // Current user (guest-safe; undefined when not logged in)
344
+ const user = Auth.userOrNull();
345
+ if (!user) return ctx.redirect("/login");
346
+
347
+ // Bearer token (API) — `plaintext` is shown once
348
+ const { plaintext, row } = await createToken({ tokenableId: user.id, name: "mobile-app" });
349
+
350
+ // Authorization is policy-based
351
+ Gate.authorize("update", post); // throws 403 unless the policy allows it
352
+ // Gate.via(PostPolicy).allows("update", post); // check without throwing
353
+ ```
354
+
355
+ Password reset, email verification, roles, 2FA (TOTP/WebAuthn), and OAuth are all
356
+ included. See [Authentication](/docs/authentication),
357
+ [Authorization](/docs/authorization), and [Roles & 2FA](/docs/roles-and-2fa).
358
+
359
+ ## Pick your frontend
360
+
361
+ Zerotal ships three rendering models; choose per route, or mix them in one app.
362
+
363
+ ### Server-rendered JSX views
364
+
365
+ Plain server-side JSX — the simplest option for content pages. See
366
+ [Views](/docs/view).
367
+
368
+ ### Flow — reactive SSR
369
+
370
+ Flow is Zerotal's reactive layer. `Component` classes run **entirely on the server**;
371
+ you write plain JSX and bind handlers directly. On each interaction the server
372
+ re-runs the method, re-renders, and streams only the changed HTML back, which Alpine.js
373
+ morphs into the DOM — no client state management, no separate API.
374
+
375
+ ```tsx
376
+ import { Component, expose, validate } from "@zerotal/flow";
377
+
378
+ export class CounterPage extends Component {
379
+ @expose count = 0;
380
+ @expose @validate((rule) => rule.required().min(2)) name = "";
381
+
382
+ @expose increment() {
383
+ this.count++;
384
+ }
385
+
386
+ override async render() {
387
+ return (
388
+ <div>
389
+ <input value={this.name} live placeholder="Your name" />
390
+ <span error={this.errors.name} class="text-red-500" />
391
+
392
+ <p>Count: {this.count}</p>
393
+
394
+ {/* Server action — round-trips and re-renders */}
395
+ <button onClick={this.increment} loadingAttr="disabled">
396
+ +
397
+ </button>
398
+
399
+ {/* Client expression — instant, no round-trip */}
400
+ <button onClick={() => this.count--}>−</button>
401
+ </div>
402
+ );
403
+ }
404
+ }
405
+ ```
406
+
407
+ A named method reference (`onClick={this.increment}`) is a **server action** — it
408
+ round-trips. An arrow function (`onClick={() => this.count--}`) is a **client
409
+ expression** — instant, no round-trip. Register `FlowProvider`, route with
410
+ `Router.flow("/counter", CounterPage)`, and read [Flow](/docs/flow) for the full
411
+ decorator and directive set.
412
+
413
+ ### Inertia — React / Vue SPA
414
+
415
+ Build a React or Vue SPA with no separate API layer. Controllers return page
416
+ responses; the Inertia client renders the matching component:
417
+
418
+ ```ts
419
+ // app/controllers/DashboardController.ts
420
+ import { inertia } from "@zerotal/inertia";
421
+ import { Post } from "../models/Post.ts";
422
+
423
+ export class DashboardController {
424
+ async index(): Promise<void> {
425
+ const posts = await Post.query().latest().limit(5).get();
426
+ return inertia("Dashboard", { posts }); // → resources/js/pages/Dashboard.tsx
427
+ }
428
+ }
429
+ ```
430
+
431
+ ```tsx
432
+ // resources/js/pages/Dashboard.tsx (React)
433
+ import { Link } from "@inertiajs/react";
434
+
435
+ export default function Dashboard({ posts }: { posts: { id: number; title: string }[] }) {
436
+ return (
437
+ <ul>
438
+ {posts.map((p) => (
439
+ <li key={p.id}>
440
+ <Link href={`/posts/${p.id}`}>{p.title}</Link>
441
+ </li>
442
+ ))}
443
+ </ul>
444
+ );
445
+ }
446
+ ```
447
+
448
+ Register `InertiaProvider` and you get full Inertia v3 support — shared props, partial
449
+ reloads, deferred props, SSR, and precognition. See [Inertia](/docs/inertia).
450
+
451
+ > **Tip** — Server views for static content, **Flow** when you want rich
452
+ > interactivity but want to stay in TypeScript on the server, and **Inertia** when
453
+ > your team already lives in React/Vue.
454
+
455
+ ## Background work: queue and jobs
456
+
457
+ Push slow work off the request. Jobs serialize to a plain payload so they survive the
458
+ queue:
459
+
460
+ ```ts
461
+ // app/jobs/SendWelcomeEmail.ts
462
+ import { Job, JobRegistry } from "@zerotal/queue";
463
+ import { User } from "../models/User.ts";
464
+ import { WelcomeNotification } from "../notifications/WelcomeNotification.ts";
465
+
466
+ export class SendWelcomeEmail extends Job {
467
+ override readonly maxAttempts = 3;
468
+ override readonly retryDelay = 5000; // ms
469
+
470
+ constructor(public readonly userId: number) {
471
+ super();
472
+ }
473
+
474
+ payload(): Record<string, unknown> {
475
+ return { userId: this.userId };
476
+ }
477
+ static fromPayload(p: Record<string, unknown>) {
478
+ return new SendWelcomeEmail(p.userId as number);
479
+ }
480
+
481
+ async handle() {
482
+ const user = await User.find(this.userId);
483
+ await user.notify(new WelcomeNotification());
484
+ }
485
+ }
486
+
487
+ JobRegistry.register(SendWelcomeEmail);
488
+ ```
489
+
490
+ ```ts
491
+ // dispatch from anywhere
492
+ import { Queue, Bus } from "@zerotal/queue";
493
+
494
+ await Queue.dispatch(new SendWelcomeEmail(user.id));
495
+
496
+ // Batch — run in parallel, then a completion job
497
+ await Bus.batch([new ProcessImage(id), new GenerateThumbnail(id)])
498
+ .then(new NotifyUploadComplete(id))
499
+ .dispatch();
500
+ ```
501
+
502
+ Run a worker with `bun zt queue:work`. There's also a
503
+ [scheduler](/docs/scheduler) for cron-style tasks. More in [Queue](/docs/queue).
504
+
505
+ ## Cache
506
+
507
+ ```ts
508
+ // in a controller or service
509
+ import { Cache } from "@zerotal/cache";
510
+
511
+ const posts = await Cache.remember("posts.recent", 60, () =>
512
+ Post.query().orderBy("created_at", "desc").limit(10).get(),
513
+ );
514
+
515
+ await Cache.put("key", value, 300);
516
+ await Cache.forget("key");
517
+
518
+ // Tag-based invalidation
519
+ await Cache.tags(["posts"]).put("post:1", post, 600);
520
+ await Cache.tags(["posts"]).flush();
521
+ ```
522
+
523
+ In-memory and Redis drivers, plus idempotency helpers. See [Cache](/docs/cache).
524
+
525
+ ## Notifications and mail
526
+
527
+ One `Notification` class fans out across channels — mail, database, broadcast, Slack,
528
+ SMS:
529
+
530
+ ```ts
531
+ // app/notifications/InvoicePaid.ts
532
+ import { Notification, MailMessage } from "@zerotal/notifications";
533
+
534
+ export class InvoicePaid extends Notification {
535
+ constructor(private invoice: Invoice) {
536
+ super();
537
+ }
538
+
539
+ via(user: User) {
540
+ return ["mail", "database"];
541
+ }
542
+
543
+ toMail(user: User) {
544
+ return new MailMessage()
545
+ .subject("Invoice paid")
546
+ .line(`Your invoice #${this.invoice.id} has been paid.`);
547
+ }
548
+ }
549
+ ```
550
+
551
+ ```ts
552
+ // The User model mixes in Notifiable, which provides .notify()
553
+ await user.notify(new InvoicePaid(invoice));
554
+ ```
555
+
556
+ See [Notifications](/docs/notifications) (mail lives here too).
557
+
558
+ ## Configuration and environment
559
+
560
+ Config lives in typed files under `config/` (auto-loaded), reading from `.env`:
561
+
562
+ ```ini
563
+ # .env
564
+ APP_ENV=development
565
+ APP_KEY=base64:… # signs Flow snapshots, encrypts sessions
566
+ DATABASE_URL=./storage/db.sqlite # or postgres://… / mysql://…
567
+ SESSION_DRIVER=cookie # or redis
568
+ ```
569
+
570
+ | Variable | Required | Description |
571
+ | ---------------- | -------- | ------------------------------------------------------------ |
572
+ | `APP_KEY` | Yes | 32-byte secret — signs Flow snapshots, encrypts sessions |
573
+ | `DATABASE_URL` | Yes | `postgres://…`, `mysql://…`, or `sqlite:./storage/db.sqlite` |
574
+ | `APP_ENV` | No | `development` (default) or `production` |
575
+ | `APP_URL` | No | Full base URL (used in mail links, etc.) |
576
+ | `SESSION_DRIVER` | No | `cookie` (default) or `redis` |
577
+ | `MAIL_DRIVER` | No | `smtp`, `resend`, or `log` |
578
+ | `PORT` | No | HTTP server port (default `3000`) |
579
+
580
+ Generate or rotate the key with `bun zt key:generate`. Full system in
581
+ [Configuration](/docs/config-system).
582
+
583
+ ## Testing
584
+
585
+ First-class HTTP, database, and fake helpers:
586
+
587
+ ```ts
588
+ // tests/posts.test.ts
589
+ import { createTestApp, Factory, assertDatabaseHas } from "@zerotal/testing";
590
+ import { NotificationFake } from "@zerotal/notifications";
591
+ import app from "../bootstrap/app.ts";
592
+ import { User } from "../app/models/User.ts";
593
+
594
+ const UserFactory = Factory.define(User, (f) => ({
595
+ name: f.string(10),
596
+ email: f.email(),
597
+ password: "password",
598
+ }));
599
+
600
+ const testApp = await createTestApp(() => app);
601
+ const user = await UserFactory.create();
602
+
603
+ const res = await testApp.actingAs(user).get("/posts");
604
+ res.assertOk();
605
+
606
+ // Fake notifications (mail is a channel)
607
+ const fake = NotificationFake.install();
608
+ await testApp.post("/register", { email: "alice@example.com" });
609
+ await assertDatabaseHas("users", { email: "alice@example.com" });
610
+ fake.assertSentTo(user, WelcomeNotification);
611
+ fake.restore();
612
+
613
+ await testApp.close();
614
+ ```
615
+
616
+ Run with `bun test`. More in [Testing](/docs/testing).
617
+
618
+ ## The zt CLI
619
+
620
+ Everything runs through `zt.ts`. The essentials:
621
+
622
+ ```bash
623
+ # in your project root
624
+ bun zt make:model Post --migration # model + migration
625
+ bun zt make:controller PostController
626
+ bun zt make:provider AppServiceProvider
627
+ bun zt make:job SendWelcomeEmail
628
+ bun zt make:page Dashboard # Inertia page
629
+ bun zt migrate # run pending migrations
630
+ bun zt migrate:rollback # roll back last batch
631
+ bun zt migrate:fresh # drop all + re-migrate
632
+ bun zt key:generate # fresh APP_KEY
633
+ bun zt queue:work # start the queue worker
634
+ bun zt serve --dev # dev server (aliased by `bun run dev`)
635
+ bun zt list # every available command
636
+ ```
637
+
638
+ More in [Commands](/docs/commands).
639
+
640
+ ## What's in the box
641
+
642
+ Zerotal is a monorepo of composable `@zerotal/*` packages — register only what you
643
+ need:
644
+
645
+ | Area | Packages / features |
646
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
647
+ | **HTTP core** | Container, router, middleware, events, config, facades, JSX views (`@zerotal/core`) |
648
+ | **Data** | ORM, migrations, query builder, [validation](/docs/validator) |
649
+ | **Auth & security** | [Sessions](/docs/session), [auth](/docs/authentication), tokens, [policies](/docs/authorization), [roles & 2FA](/docs/roles-and-2fa), WebAuthn, OAuth, [CSRF](/docs/csrf) |
650
+ | **Frontend** | Flow, flow-ui component library, Inertia |
651
+ | **Services** | [Cache](/docs/cache), [queue](/docs/queue), [scheduler](/docs/scheduler), [storage](/docs/storage), [broadcasting](/docs/broadcasting), [notifications & mail](/docs/notifications) |
652
+ | **Cross-cutting** | [Telemetry](/docs/telemetry), [HTTP client](/docs/client), [i18n](/docs/i18n), [tenancy](/docs/tenancy), [audit](/docs/audit), [monitor](/docs/monitor) |
653
+ | **Tooling** | Admin panel, devtools, [testing](/docs/testing) helpers, `create-zerotal` scaffolder |
654
+
655
+ The [README](../README.md) has a package-by-package table with links.
656
+
657
+ ## Working in the codebase
658
+
659
+ - **The `zt` CLI is your control panel.** `bun zt list` shows everything —
660
+ scaffolding, migrations, the dev server, the worker, and tests all run through it.
661
+ - **No build, ever.** `bun run dev` / `bun test` / `bun run typecheck`. No compile
662
+ step to remember.
663
+ - **Reference apps are the best teacher.** Full working apps under `apps/` exercise the
664
+ framework end-to-end — a Flow-based finance app and a Flow + Auth + ORM starter
665
+ are the most complete real-world examples. Read them alongside the docs.
666
+ - **Conventions are documented, not magic.** When something "just works" (a model you
667
+ never registered, a policy suddenly enforced), [Conventions](/docs/conventions)
668
+ explains exactly what the framework discovered and why.
669
+
670
+ ## Next steps
671
+
672
+ - [Getting Started](/docs/getting-started) — scaffold and run your first app.
673
+ - [Directory Structure](/docs/structure) & [Conventions](/docs/conventions) — where things go and why they wire up.
674
+ - [Request Lifecycle](/docs/lifecycle), [Container](/docs/container), [Providers](/docs/providers) — how boot and wiring work.
675
+ - [ORM](/docs/orm) & [Query Builder](/docs/query-builder) — the data layer in depth.
676
+ - [Flow](/docs/flow) / [Inertia](/docs/inertia) — pick and learn your frontend model.