@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- 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.
|