@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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting Started
|
|
3
|
+
description: Scaffold, configure, and run your first Zerotal application on Bun in a few minutes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting Started
|
|
7
|
+
|
|
8
|
+
Zerotal is a full-stack framework for [Bun](https://bun.sh). This guide walks you
|
|
9
|
+
from a blank machine to a running app with a route, a model, and a service
|
|
10
|
+
provider.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **Bun** ≥ 1.1 — [install](https://bun.sh/docs/installation)
|
|
15
|
+
- A PostgreSQL, MySQL, or SQLite database (SQLite requires nothing extra)
|
|
16
|
+
|
|
17
|
+
## Create a new project
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# in your project root's parent directory
|
|
21
|
+
bun create zerotal my-app # or: bunx create-zerotal my-app
|
|
22
|
+
cd my-app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The scaffolder prompts for a project name and a template, then generates a
|
|
26
|
+
ready-to-run project and installs dependencies. The **database** prompt only
|
|
27
|
+
appears for the API template:
|
|
28
|
+
|
|
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 |
|
|
34
|
+
|
|
35
|
+
It writes a fresh `APP_KEY` into the generated `.env.example` for you (no manual
|
|
36
|
+
generation needed). For Postgres/MySQL it reminds you to set `DATABASE_URL`
|
|
37
|
+
before migrating.
|
|
38
|
+
|
|
39
|
+
### Which template should I use?
|
|
40
|
+
|
|
41
|
+
- **API** — JSON REST API with core, [ORM](/docs/orm), [auth](/docs/authentication),
|
|
42
|
+
[validation](/docs/validator), and [testing](/docs/testing/index). The default for a backend service.
|
|
43
|
+
- **Flow** — server-driven reactive UI ([Flow](/docs/flow) pages, top nav, Tailwind).
|
|
44
|
+
- **React** — [Inertia](/docs/inertia) + React SPA with file-based routes and Tailwind.
|
|
45
|
+
- **Vue** — Inertia + Vue SPA with file-based routes and Tailwind.
|
|
46
|
+
- **Minimal** — a single page with JSX views and Tailwind on the bare framework.
|
|
47
|
+
|
|
48
|
+
> **Note** — Only the **API** template ships a database config and migrations.
|
|
49
|
+
> The Flow, React, Vue, and Minimal templates start without a database.
|
|
50
|
+
|
|
51
|
+
## Project structure
|
|
52
|
+
|
|
53
|
+
The **API** template generates the following. Other templates vary (SPA
|
|
54
|
+
templates add a `resources/` frontend, for example):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
# my-app/ (API template)
|
|
58
|
+
my-app/
|
|
59
|
+
├── app/
|
|
60
|
+
│ ├── controllers/ # HTTP controllers
|
|
61
|
+
│ ├── middleware/ # Custom middleware
|
|
62
|
+
│ └── models/ # ORM models (auto-discovered)
|
|
63
|
+
├── bootstrap/
|
|
64
|
+
│ └── app.ts # Builds the Application (providers + routing)
|
|
65
|
+
├── config/ # Config files (database, session, queue…) — auto-loaded
|
|
66
|
+
├── database/
|
|
67
|
+
│ └── migrations/ # Database migrations
|
|
68
|
+
├── routes/
|
|
69
|
+
│ └── index.ts # Route definitions
|
|
70
|
+
├── tests/ # Test suites
|
|
71
|
+
├── zt.ts # Managed CLI entry point — do not edit
|
|
72
|
+
└── .env # Environment variables (git-ignored)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`zt.ts` is the entry point for **everything** — `bun zt serve`, `migrate`,
|
|
76
|
+
`test`, and every `make:*` generator run through it. It boots `bootstrap/app.ts`,
|
|
77
|
+
which defines your [Application](/docs/application). Anything under `app/models/`,
|
|
78
|
+
`app/providers/`, and `config/` is [auto-discovered](/docs/conventions) at boot —
|
|
79
|
+
no manual registration.
|
|
80
|
+
|
|
81
|
+
> **Tip** — `app/providers/` is auto-discovered when present. The API template
|
|
82
|
+
> doesn't generate one; run `bun zt make:provider …` and the framework picks it
|
|
83
|
+
> up at boot.
|
|
84
|
+
|
|
85
|
+
## Environment setup
|
|
86
|
+
|
|
87
|
+
The scaffolder generates `.env.example` with a ready-made `APP_KEY`. Copy it to
|
|
88
|
+
`.env`:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# in your project root
|
|
92
|
+
cp .env.example .env
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ini
|
|
96
|
+
# .env
|
|
97
|
+
APP_ENV=development
|
|
98
|
+
APP_KEY=base64:… # pre-generated; rotate with `bun zt key:generate`
|
|
99
|
+
DATABASE_URL=./database/db.sqlite
|
|
100
|
+
SESSION_SECRET=change-me-in-production
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The database is configured with a single **`DATABASE_URL`** — SQLite uses a file
|
|
104
|
+
path, Postgres/MySQL use a connection string:
|
|
105
|
+
|
|
106
|
+
```ini
|
|
107
|
+
# .env — alternative DATABASE_URL forms
|
|
108
|
+
# DATABASE_URL=postgres://user:pass@localhost:5432/my_app
|
|
109
|
+
# DATABASE_URL=mysql://root@localhost:3306/my_app
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Danger** — Change `SESSION_SECRET` before deploying. The placeholder value is
|
|
113
|
+
> not safe for production.
|
|
114
|
+
|
|
115
|
+
See [Configuration](/docs/config-system) for the full config system.
|
|
116
|
+
|
|
117
|
+
## Run migrations
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# in your project root
|
|
121
|
+
bun zt migrate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Start the dev server
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# in your project root
|
|
128
|
+
bun dev # → bun zt serve --dev
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Your app is now running at **http://localhost:3000** with hot reload enabled —
|
|
132
|
+
`--dev` watches your files and hot-swaps routes without dropping connections. The
|
|
133
|
+
generated `package.json` also gives you `bun start` (production serve) and
|
|
134
|
+
`bun test`.
|
|
135
|
+
|
|
136
|
+
## Your first route
|
|
137
|
+
|
|
138
|
+
Open `routes/index.ts` and add:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// routes/index.ts
|
|
142
|
+
import { Router } from "zerotal";
|
|
143
|
+
import { PostController } from "../app/controllers/PostController.ts";
|
|
144
|
+
|
|
145
|
+
Router.get("/posts", PostController, "index");
|
|
146
|
+
Router.post("/posts", PostController, "store");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Create the controller:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// app/controllers/PostController.ts
|
|
153
|
+
import type { HttpContext } from "zerotal";
|
|
154
|
+
|
|
155
|
+
export class PostController {
|
|
156
|
+
async index(ctx: HttpContext) {
|
|
157
|
+
return ctx.json({ posts: [] });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async store(ctx: HttpContext) {
|
|
161
|
+
const body = await ctx.request.json();
|
|
162
|
+
return ctx.json({ created: body }, 201);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> **Note** — Every controller action receives the request
|
|
168
|
+
> [HttpContext](/docs/context) directly. Read route params and resolved model
|
|
169
|
+
> bindings from `ctx.params`; type them with the generic, e.g.
|
|
170
|
+
> `ctx: HttpContext<{ post: Post }>`.
|
|
171
|
+
|
|
172
|
+
## Your first model
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# in your project root
|
|
176
|
+
bun zt make:model Post --migration
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This generates `app/models/Post.ts` and a matching migration. Models in
|
|
180
|
+
`app/models/` are auto-discovered at boot — no manual registration, and the table
|
|
181
|
+
name is derived by convention (`Post` → `posts`), so `@table` is optional. The
|
|
182
|
+
same applies to observers, policies, and event listeners; see
|
|
183
|
+
[Conventions](/docs/conventions).
|
|
184
|
+
|
|
185
|
+
Open the migration and define your columns:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// database/migrations/xxxx_create_posts_table.ts
|
|
189
|
+
import { Migration, Schema } from "@zerotal/orm";
|
|
190
|
+
|
|
191
|
+
export default class CreatePostsTable extends Migration {
|
|
192
|
+
async up(): Promise<void> {
|
|
193
|
+
await Schema.create("posts", (table) => {
|
|
194
|
+
table.increments("id");
|
|
195
|
+
table.string("title");
|
|
196
|
+
table.text("body");
|
|
197
|
+
table.string("slug").unique();
|
|
198
|
+
table.timestamps();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async down(): Promise<void> {
|
|
203
|
+
await Schema.drop("posts");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Run it:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# in your project root
|
|
212
|
+
bun zt migrate
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Your model is ready to use:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// in a controller or service
|
|
219
|
+
import { Post } from "../models/Post.ts";
|
|
220
|
+
|
|
221
|
+
const posts = await Post.query().latest().limit(10).get();
|
|
222
|
+
const post = await Post.find(1);
|
|
223
|
+
const fresh = await Post.create({ title: "Hello", body: "...", slug: "hello" });
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Adding a service provider
|
|
227
|
+
|
|
228
|
+
Providers are where you wire up your own bindings. Scaffold one:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# in your project root
|
|
232
|
+
bun zt make:provider AppServiceProvider
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// app/providers/AppServiceProvider.ts
|
|
237
|
+
import { ServiceProvider } from "zerotal";
|
|
238
|
+
import { PaymentGateway } from "../services/PaymentGateway.ts";
|
|
239
|
+
import { StripeGateway } from "../services/StripeGateway.ts";
|
|
240
|
+
|
|
241
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
242
|
+
onRegister(): void {
|
|
243
|
+
this.app.container.singleton(
|
|
244
|
+
PaymentGateway,
|
|
245
|
+
() => new StripeGateway({ key: Bun.env.STRIPE_KEY! }),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Anything under `app/providers/` is **auto-discovered and registered at boot** — no
|
|
252
|
+
manual wiring needed. You only touch `bootstrap/app.ts` when you need to control
|
|
253
|
+
_ordering_ (e.g. one provider must register before another). See
|
|
254
|
+
[Service Providers](/docs/providers) and [Conventions](/docs/conventions).
|
|
255
|
+
|
|
256
|
+
## Available commands
|
|
257
|
+
|
|
258
|
+
Everything runs through `zt.ts`. The most common:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# in your project root
|
|
262
|
+
bun zt make:model Name --migration # model + migration
|
|
263
|
+
bun zt make:controller Name # controller class
|
|
264
|
+
bun zt make:middleware Name # middleware class
|
|
265
|
+
bun zt make:provider Name # service provider
|
|
266
|
+
bun zt make:job Name # queue job
|
|
267
|
+
bun zt migrate # run pending migrations
|
|
268
|
+
bun zt migrate:rollback # roll back last batch
|
|
269
|
+
bun zt migrate:status # show migration status
|
|
270
|
+
bun zt migrate:fresh # roll back all + re-migrate
|
|
271
|
+
bun zt key:generate # generate a fresh APP_KEY
|
|
272
|
+
bun zt queue:work # start the queue worker
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
There are many more (`make:policy`, `make:factory`, `make:seeder`, `make:page`,
|
|
276
|
+
`db:seed`, `schedule:list`, …). Run `bun zt list` to see them all, or see
|
|
277
|
+
[Commands](/docs/commands) for the full list and
|
|
278
|
+
[Scaffolding](/docs/scaffolding) for what each generator produces.
|
|
279
|
+
|
|
280
|
+
## Next steps
|
|
281
|
+
|
|
282
|
+
- [Routing](/docs/routing) — route definitions, groups, parameters, and file-based routes.
|
|
283
|
+
- [Controllers](/docs/controllers) — move route logic into classes.
|
|
284
|
+
- [ORM](/docs/orm) — models, queries, and relationships.
|
|
285
|
+
- [Configuration](/docs/config-system) — how config and auto-discovery work.
|
|
286
|
+
- [Flow](/docs/flow) — server-driven reactive UI (or [Inertia](/docs/inertia) for a React/Vue SPA).
|
package/docs/health.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Health Checks
|
|
3
|
+
description: Serve a probe-friendly /health endpoint that reports liveness and the readiness of your app's dependencies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Health Checks
|
|
7
|
+
|
|
8
|
+
Zerotal serves a `/health` endpoint for liveness/readiness probes (Kubernetes,
|
|
9
|
+
load balancers, uptime monitors). It is **on by default outside production** and
|
|
10
|
+
**off in production until you enable it and protect it**.
|
|
11
|
+
|
|
12
|
+
Health checks are a core built-in — there is no package to install and no
|
|
13
|
+
provider to register. You configure the endpoint under the `health` key of
|
|
14
|
+
`config/app.ts`, then register dependency checks via the `Health` registry.
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
Health checks are built into `@zerotal/core` — nothing to install and no
|
|
19
|
+
provider to register. The endpoint is registered when the application starts:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Health } from "zerotal";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Defaults
|
|
26
|
+
|
|
27
|
+
| Environment | Default | Access |
|
|
28
|
+
| ---------------------------- | -------- | ------------------------------------------------------------ |
|
|
29
|
+
| development / test / staging | enabled | open |
|
|
30
|
+
| production | disabled | requires a `secret` (`?key=` or `X-Health-Key`) once enabled |
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
The health config lives under the `health` key of `config/app.ts`. Use the
|
|
35
|
+
`AppConfig()` helper so every field stays type-checked:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// config/app.ts
|
|
39
|
+
import { env } from "zerotal";
|
|
40
|
+
import { AppConfig } from "zerotal/config";
|
|
41
|
+
|
|
42
|
+
export default AppConfig({
|
|
43
|
+
name: "My App",
|
|
44
|
+
health: {
|
|
45
|
+
enabled: true, // default: on outside production, off in production
|
|
46
|
+
path: "/health", // default: '/health'
|
|
47
|
+
secret: env("HEALTH_KEY"), // required in production
|
|
48
|
+
showDetails: true, // false → bare { "status": "ok" }
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Field | Required | Default | Description |
|
|
54
|
+
| ------------- | -------- | --------------- | ---------------------------------------------------------------------- |
|
|
55
|
+
| `enabled` | no | on outside prod | Serve the endpoint. Off in production unless explicitly set. |
|
|
56
|
+
| `path` | no | `"/health"` | Route path the endpoint is registered at. |
|
|
57
|
+
| `secret` | in prod | unset | Shared secret, supplied via `?key=` or the `X-Health-Key` header. |
|
|
58
|
+
| `showDetails` | no | `true` | Include per-check details; `false` collapses the body to `{ status }`. |
|
|
59
|
+
|
|
60
|
+
> **Note** — The endpoint is only registered when the app runs in the `web` or
|
|
61
|
+
> `worker` environment. Console commands and tests never expose it.
|
|
62
|
+
|
|
63
|
+
> **Note** — A legacy `config/health.ts` namespace and a bare `health: true`
|
|
64
|
+
> boolean on `config/app.ts` are still honoured for back-compat. New projects
|
|
65
|
+
> should author the `health` object directly under `config/app.ts`.
|
|
66
|
+
|
|
67
|
+
### Protecting the endpoint
|
|
68
|
+
|
|
69
|
+
In production the endpoint refuses to serve unless a `secret` is configured —
|
|
70
|
+
so it can never be exposed unprotected by accident. Call it with the key:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# from a probe or terminal
|
|
74
|
+
curl "https://your-app.com/health?key=YOUR_HEALTH_KEY"
|
|
75
|
+
# or via header
|
|
76
|
+
curl -H "X-Health-Key: YOUR_HEALTH_KEY" https://your-app.com/health
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Access control resolves as follows:
|
|
80
|
+
|
|
81
|
+
- **A `secret` is set** — the request must supply a matching `?key=` or
|
|
82
|
+
`X-Health-Key`. A wrong or missing key returns **HTTP 401**.
|
|
83
|
+
- **Production with no `secret`** — the endpoint is refused with **HTTP 503**;
|
|
84
|
+
it must be protected before it can be exposed.
|
|
85
|
+
- **Otherwise** (development, no secret) — access is open.
|
|
86
|
+
|
|
87
|
+
You can also leave `secret` unset and instead wrap the path in your own
|
|
88
|
+
auth/IP-allowlist middleware.
|
|
89
|
+
|
|
90
|
+
> **Danger** — In production, set a `secret` (or wrap the path in your own auth)
|
|
91
|
+
> before enabling the endpoint. The detailed report exposes dependency status,
|
|
92
|
+
> versions, and memory; an unprotected `/health` leaks that to anyone.
|
|
93
|
+
|
|
94
|
+
## Registering checks
|
|
95
|
+
|
|
96
|
+
Out of the box the endpoint is a **liveness** probe (the process answered) plus
|
|
97
|
+
a built-in `runtime` probe (memory, Bun version, in-flight requests). It becomes
|
|
98
|
+
a **readiness** probe as you register dependency checks against the `Health`
|
|
99
|
+
registry exported from `zerotal`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// in a bootstrap file (e.g. bootstrap/health.ts)
|
|
103
|
+
import { Health } from "zerotal/health";
|
|
104
|
+
|
|
105
|
+
// Critical checks drive the overall status and the HTTP 503 response.
|
|
106
|
+
Health.register(
|
|
107
|
+
"database",
|
|
108
|
+
async () => {
|
|
109
|
+
await DB.raw("select 1");
|
|
110
|
+
},
|
|
111
|
+
{ critical: true },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Non-critical failures degrade the report without failing readiness.
|
|
115
|
+
Health.register("cache", async () => {
|
|
116
|
+
await Cache.put("__health", "1", 5);
|
|
117
|
+
return { meta: { driver: "redis" } };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
Health.register("disk", () => {
|
|
121
|
+
const free = getFreeBytes();
|
|
122
|
+
return free < 100_000_000
|
|
123
|
+
? { status: "degraded", message: "low disk" }
|
|
124
|
+
: { status: "ok", meta: { freeBytes: free } };
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A check is healthy when it returns (or returns `{ status: 'ok' }`), `degraded`
|
|
129
|
+
when it returns `{ status: 'degraded' }`, and `down` when it returns
|
|
130
|
+
`{ status: 'down' }` **or throws** (the thrown message becomes the check's
|
|
131
|
+
`message`).
|
|
132
|
+
|
|
133
|
+
### Overall status
|
|
134
|
+
|
|
135
|
+
The aggregate status is derived from every check:
|
|
136
|
+
|
|
137
|
+
- **`down`** (HTTP 503) — any `critical` check is down.
|
|
138
|
+
- **`degraded`** (HTTP 200) — a non-critical check is down or degraded.
|
|
139
|
+
- **`ok`** (HTTP 200) — everything passed.
|
|
140
|
+
|
|
141
|
+
> **Tip** — Mark only the dependencies your app genuinely cannot serve requests
|
|
142
|
+
> without (your primary database, say) as `critical`. A flaky cache should
|
|
143
|
+
> `degrade` the report, not knock the whole app out of the load balancer.
|
|
144
|
+
|
|
145
|
+
## What the endpoint returns
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"status": "ok",
|
|
150
|
+
"app": { "name": "my-app", "version": "1.4.0", "environment": "production" },
|
|
151
|
+
"uptime": 3672,
|
|
152
|
+
"timestamp": "2026-06-15T10:32:00.000Z",
|
|
153
|
+
"checks": {
|
|
154
|
+
"runtime": {
|
|
155
|
+
"status": "ok",
|
|
156
|
+
"durationMs": 0,
|
|
157
|
+
"critical": false,
|
|
158
|
+
"meta": { "memory": { "rss": 84934656 }, "bun": "1.3.14", "pendingRequests": 3 }
|
|
159
|
+
},
|
|
160
|
+
"database": { "status": "ok", "durationMs": 4, "critical": true },
|
|
161
|
+
"cache": { "status": "ok", "durationMs": 2, "critical": false, "meta": { "driver": "redis" } }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`uptime` is whole seconds since the process started. With `showDetails: false`
|
|
167
|
+
the body collapses to `{ "status": "ok" }` — handy for public uptime monitors
|
|
168
|
+
that should not see internal details.
|
|
169
|
+
|
|
170
|
+
## Testing
|
|
171
|
+
|
|
172
|
+
Set your suite up once as described in [Testing](/docs/testing). A health check
|
|
173
|
+
is a function you registered, so test it directly — and test what happens when it
|
|
174
|
+
fails, since that is the case the endpoint exists for.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// tests/health/checks.test.ts
|
|
178
|
+
import { test, expect, afterEach } from "bun:test";
|
|
179
|
+
import { Health } from "zerotal";
|
|
180
|
+
|
|
181
|
+
afterEach(() => Health.clear());
|
|
182
|
+
|
|
183
|
+
test("a failing critical check brings the report down", async () => {
|
|
184
|
+
Health.register(
|
|
185
|
+
"database",
|
|
186
|
+
async () => {
|
|
187
|
+
throw new Error("connection refused");
|
|
188
|
+
},
|
|
189
|
+
{ critical: true },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const report = await Health.run({ name: "app", version: "1.0.0", environment: "test" });
|
|
193
|
+
|
|
194
|
+
expect(report.status).toBe("down");
|
|
195
|
+
expect(report.checks.database?.status).toBe("down");
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**`Health.clear()` in `afterEach` matters.** The registry is process-wide, so a
|
|
200
|
+
check registered by one test runs in every later one — and a deliberately failing
|
|
201
|
+
check left behind turns the rest of your suite red for reasons that have nothing
|
|
202
|
+
to do with it.
|
|
203
|
+
|
|
204
|
+
**Critical and non-critical fail differently**, and that distinction is the whole
|
|
205
|
+
design. A non-critical check that fails must leave the overall status up,
|
|
206
|
+
otherwise a flaky cache probe takes your deployment out of the load balancer:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// tests/health/checks.test.ts
|
|
210
|
+
test("a non-critical failure degrades without going down", async () => {
|
|
211
|
+
Health.register("cache", async () => {
|
|
212
|
+
throw new Error("timeout");
|
|
213
|
+
}); // critical defaults to false
|
|
214
|
+
|
|
215
|
+
const report = await Health.run(meta);
|
|
216
|
+
|
|
217
|
+
expect(report.status).not.toBe("down");
|
|
218
|
+
expect(report.checks.cache?.status).toBe("down");
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**The endpoint's access rules deserve their own test**, because a health endpoint
|
|
223
|
+
that leaks internals is a reconnaissance gift:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// tests/http/health.test.ts
|
|
227
|
+
const res = await app.get("/health");
|
|
228
|
+
|
|
229
|
+
res.assertOk();
|
|
230
|
+
res.assertDontSee("connection refused"); // no error detail to an anonymous caller
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> **Note** — Check functions run on every request to the endpoint. A check that
|
|
234
|
+
> queries the database on each call turns your load balancer's probe into
|
|
235
|
+
> steady traffic — worth a test asserting yours is cheap, or a cache in front.
|
|
236
|
+
|
|
237
|
+
## References
|
|
238
|
+
|
|
239
|
+
### `Health` registry
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { Health } from "zerotal/health";
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
| Method | Signature | Description |
|
|
246
|
+
| ---------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
247
|
+
| `register` | `register(name: string, fn: HealthCheckFn, options?: { critical?: boolean }): this` | Register (or replace) a named check; `critical` checks fail readiness. |
|
|
248
|
+
| `remove` | `remove(name: string): this` | Remove a previously registered check by name. |
|
|
249
|
+
| `clear` | `clear(): this` | Remove every registered check. |
|
|
250
|
+
| `has` | `has(name: string): boolean` | Whether a check is registered under the given name. |
|
|
251
|
+
| `names` | `names: string[]` | The names of all registered checks. |
|
|
252
|
+
| `run` | `run(meta: HealthRunMeta): Promise<HealthReport>` | Run every check and assemble the aggregate report. |
|
|
253
|
+
|
|
254
|
+
### `HealthResult`
|
|
255
|
+
|
|
256
|
+
What a check returns to describe its own state. A check may also return nothing
|
|
257
|
+
(or `undefined`) to mean `ok`.
|
|
258
|
+
|
|
259
|
+
| Field | Type | Description |
|
|
260
|
+
| --------- | ------------------------------ | ------------------------------------------------------- |
|
|
261
|
+
| `status` | `"ok" \| "degraded" \| "down"` | Defaults to `ok` when a check returns without throwing. |
|
|
262
|
+
| `message` | `string` | Optional human-readable detail. |
|
|
263
|
+
| `meta` | `Record<string, unknown>` | Optional structured metadata surfaced in the report. |
|
|
264
|
+
|
|
265
|
+
## Next steps
|
|
266
|
+
|
|
267
|
+
- [Telemetry](/docs/telemetry) — collect the metrics behind your readiness checks.
|
|
268
|
+
- [Deployment](/docs/deployment) — wire `/health` into liveness and readiness probes.
|
|
269
|
+
- [Logger](/docs/logger) — record failing checks for later inspection.
|