@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,434 @@
1
+ ---
2
+ title: Deployment
3
+ description: Take a Zerotal app to production — environment, migrations, assets, the server process, a reverse proxy, and a worker.
4
+ ---
5
+
6
+ # Deployment
7
+
8
+ A Zerotal app is a Bun process. Deploying it means: install dependencies, set
9
+ production environment variables, run migrations, build frontend assets if you have
10
+ them, and start the server (plus a worker if you use queues or the scheduler).
11
+
12
+ If a reverse proxy sits in front of it — and in most deployments one does — read
13
+ [Behind a reverse proxy](#behind-a-reverse-proxy) before you launch. A proxied app has
14
+ one failure mode that a green test suite cannot see.
15
+
16
+ ## `bun zt deploy:<env>`
17
+
18
+ One command runs the release and refuses to finish it when something is wrong:
19
+
20
+ ```bash
21
+ # on the box, with that environment's variables loaded
22
+ APP_ENV=production bun zt deploy:production
23
+ ```
24
+
25
+ It runs four phases, and **everything that can refuse runs before anything that
26
+ mutates** — a bad origin list stops the deploy while the old release is still
27
+ serving, rather than after the migration has run:
28
+
29
+ | Phase | What it does |
30
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
31
+ | Preflight | Checks this process really is that environment, re-runs every config validator with production semantics, then runs `zt doctor` |
32
+ | Build | `assets:build`, and `inertia:build --production` if the app has Inertia |
33
+ | Migrate | `migrate` — skip with `--skip-migrations` |
34
+ | Verify | `zt doctor` again, now that the schema has one story |
35
+
36
+ It exits non-zero on any failure and **does not restart your service**. That is
37
+ deliberate: systemd, your container runtime or your deploy script owns process
38
+ lifecycle, and this gives it a gate to restart behind.
39
+
40
+ ```bash
41
+ bun zt deploy:production --dry-run # print the plan, run none of it
42
+ bun zt deploy:production --skip-migrations # release without touching the schema
43
+ bun zt deploy:production --probe=https://example.com # real handshake at the end
44
+ ```
45
+
46
+ Every environment gets its own command. `production` and `staging` exist by default;
47
+ declare more — or give one a URL and its own steps — in `config/deploy.ts`:
48
+
49
+ ```ts
50
+ import { DeployConfig } from "zerotal/config";
51
+
52
+ export default DeployConfig({
53
+ targets: {
54
+ production: { url: "https://example.com" },
55
+ staging: { url: "https://staging.example.com" },
56
+ },
57
+ });
58
+ ```
59
+
60
+ The target name is checked against the deployment this process was started as, so
61
+ `deploy:production` on a staging box stops on the first line instead of migrating
62
+ the wrong database.
63
+
64
+ Each entry is a `DeployTarget`:
65
+
66
+ | Field | Meaning |
67
+ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `url` | The public URL in this environment. What `--probe` handshakes against when given no URL of its own. |
69
+ | `steps` | Override the release steps. Defaults to `DEFAULT_DEPLOY_STEPS` — `assets:build`, `inertia:build`, `migrate`. Each names a `zt` command, and one that is not registered is skipped, so an app without Inertia simply has no Inertia step. |
70
+
71
+ Omit the file entirely and you get `DEFAULT_DEPLOY_TARGETS`: `production` and
72
+ `staging`, both with the default steps.
73
+
74
+ > **Note** — `deploy:<env>` runs **where the app runs**, with that environment's
75
+ > variables. It does not reach another machine over SSH. Run it on the box, or in
76
+ > the container build, as the step before the restart.
77
+
78
+ ## Production checklist
79
+
80
+ The command above automates most of this. The list is what it runs, and what it
81
+ cannot do for you.
82
+
83
+ 1. **`APP_ENV=production`** — disables dev-only behavior (N+1 warnings, verbose
84
+ errors, auto-`synchronize`) and normalizes to the `web` runtime mode.
85
+ `staging` counts as production for all of it.
86
+ 2. **Set a strong `APP_KEY`** — required for encryption, signed URLs, and sessions.
87
+ Generate one with `bun zt key:generate` and store it as a secret.
88
+ 3. **Set `APP_URL` to the public URL** — the origin browsers actually reach the app on.
89
+ Endpoints that bypass the middleware pipeline are checked against it.
90
+ 4. **Point `DATABASE_URL`** at your production database.
91
+ 5. **Set `app.secureHeaders.secure: true`** once you serve over HTTPS, or no
92
+ `Strict-Transport-Security` header is sent at all.
93
+ 6. **Name your CORS origins** — `app.cors.origin: "*"` lets any site read this app's
94
+ responses out of a visitor's browser.
95
+ 7. **Run migrations** — never rely on auto-`synchronize` in production (it's
96
+ hard-off there); ship migration files instead.
97
+ 8. **Build frontend assets** as a release step, not at boot.
98
+ 9. **Start the server**, and a **worker** if you use queues/scheduling.
99
+ 10. **Run `bun zt doctor --url=…`** against the deployed site once it is live — the
100
+ one check that cannot run before the cutover.
101
+
102
+ Items 1–8 are what `deploy:<env>` checks or does. Starting the process and probing
103
+ the live site are yours.
104
+
105
+ ### What `--url` sees that nothing else can
106
+
107
+ `bun zt doctor` on its own reads the app from the inside, where the app is right about
108
+ itself. `--url` fetches the deployed site back through whatever proxy is in front of it,
109
+ and reports two things only that round trip can show:
110
+
111
+ - **The WebSocket transport**, handshaked as a browser would. A proxy that gates or never
112
+ forwards the upgrade leaves the app healthy from the inside — the HTML renders, the logs
113
+ are quiet — while every action in the browser silently does nothing.
114
+ - **Security headers sent twice.** A header the app sets and the proxy also sets is
115
+ invisible from inside the process. `X-Frame-Options: DENY` from the proxy plus
116
+ `SAMEORIGIN` from the app is a real deployment this found, and browsers do not agree on
117
+ which one applies — a control enforced inconsistently, which is worse than one that is
118
+ simply missing, because it looks configured. Conflicting values fail; identical
119
+ duplicates warn.
120
+
121
+ The fix for a duplicate is always the same shape: pick one place — `app.secureHeaders` in
122
+ `config/app.ts`, or the proxy — and remove the other.
123
+
124
+ ## Environment
125
+
126
+ Set configuration through environment variables (not a committed `.env`). At minimum:
127
+
128
+ ```ini
129
+ # environment variables (set as platform secrets, not a committed .env)
130
+ APP_ENV=production
131
+ APP_URL=https://your.app
132
+ APP_KEY=… # bun zt key:generate writes a base64 key
133
+ DATABASE_URL=postgres://user:pass@db-host:5432/app
134
+ ```
135
+
136
+ `key:generate` writes a raw 32-byte base64 key into `.env`; both a raw base64 string
137
+ and a `base64:`-prefixed one are accepted. Generate it **on the server** — a key carried
138
+ from a laptop is a key that has been in a shell history and a scrollback buffer.
139
+
140
+ `APP_ENV` accepts deployment names like `production` and `staging`; they all normalize to
141
+ the `web` runtime mode — they describe _where_ the app runs, not _how_. See
142
+ [Configuration](/docs/config-system).
143
+
144
+ > **Danger** — store `APP_KEY` as a managed secret, never in a committed file. Losing or rotating it invalidates encrypted values, signed URLs, and active sessions.
145
+
146
+ ## Run migrations
147
+
148
+ Run pending migrations as part of each release, before the new server starts taking
149
+ traffic:
150
+
151
+ ```bash
152
+ # in your project root
153
+ bun zt migrate
154
+ ```
155
+
156
+ Auto-`synchronize` is **hard-off in production** — generate and commit
157
+ [migrations](/docs/migrations) during development and run them on deploy.
158
+
159
+ ## Build assets
160
+
161
+ Build the frontend bundle as a release step, so `public/` holds compiled output before
162
+ the process starts:
163
+
164
+ ```bash
165
+ # in your project root
166
+ bun zt assets:build # every bundle this app declares
167
+ bun zt inertia:build --production # Inertia (React/Vue) instead
168
+ ```
169
+
170
+ `assets:build` covers both sources of bundles: the entrypoints named in `app.assets`, and
171
+ Flow's conventional `resources/css/app.css` and `resources/js/app.js`.
172
+
173
+ Outside production, `serve` builds these at boot so there is nothing to remember in
174
+ development. In production it builds them only if the output directory is writable — so a
175
+ release that built its assets ahead of time and locked the tree down serves what it
176
+ shipped, and logs one line saying so. That is what lets the service run under a properly
177
+ hardened unit; see [Hardening the service](#hardening-the-service).
178
+
179
+ Bump your asset version (or hash the bundle) so clients reload onto the new build — see
180
+ [Inertia › Asset versioning](/docs/inertia/middleware#asset-versioning).
181
+
182
+ ## Start the server
183
+
184
+ ```bash
185
+ # in your project root
186
+ bun zt serve # binds 0.0.0.0:3000
187
+ bun zt serve --port=8080 # custom port
188
+ ```
189
+
190
+ Run this under a process supervisor (systemd, Docker, Fly.io, Railway, a PaaS, …) so
191
+ it restarts on crash. The server installs `SIGTERM`/`SIGINT` handlers and drains
192
+ gracefully on shutdown, which works cleanly with rolling deploys.
193
+
194
+ Set the port explicitly on a host that already runs something. The default is 3000, which
195
+ is also what the last app you deployed is using.
196
+
197
+ ### Worker process
198
+
199
+ If you use [queues](/docs/queue) or the [scheduler](/docs/scheduler), run a separate
200
+ worker process so background work is isolated from request handling and can be scaled
201
+ independently:
202
+
203
+ ```bash
204
+ # in your project root
205
+ bun zt worker
206
+ ```
207
+
208
+ This boots in the `worker` environment (no HTTP server) and drains in-flight jobs on
209
+ shutdown. For small deployments you can instead poll inline from a provider's
210
+ `onStarted()` — see [Scheduler › Running the worker](/docs/scheduler#running-the-worker) —
211
+ but a dedicated process is recommended for production.
212
+
213
+ ## Behind a reverse proxy
214
+
215
+ Proxying introduces one failure that nothing in your development loop can reproduce, so
216
+ it is worth understanding rather than just copying the config below.
217
+
218
+ ### Why a proxied app needs `APP_URL`
219
+
220
+ Two kinds of request bypass the middleware pipeline and therefore carry their own origin
221
+ check: **WebSocket upgrades**, and **raw routes** — of which Flow's `/__flow/http` action
222
+ fallback is one. Both are credentialed and neither is protected by
223
+ [CSRF middleware](/docs/csrf), so each compares the browser's `Origin` header against the
224
+ origins the app accepts.
225
+
226
+ The app's own origin is always accepted — but "own" means the origin of the request URL,
227
+ which behind a proxy is the loopback address it bound to (`http://127.0.0.1:3002`), never
228
+ the public URL the browser sends. So the public origin has to come from config, and it
229
+ does: `AppConfig()` fills `app.allowedOrigins` from `url`.
230
+
231
+ ```ts
232
+ // config/app.ts
233
+ export default AppConfig({
234
+ name: "My App",
235
+ url: env("APP_URL", "http://localhost:3000"), // ← allowedOrigins derives from this
236
+ });
237
+ ```
238
+
239
+ Name additional origins only when a genuinely different host drives the app — an SPA on
240
+ `app.example.com` calling `api.example.com`. What you pass is added to the URL's origin
241
+ rather than replacing it:
242
+
243
+ ```ts
244
+ // config/app.ts
245
+ export default AppConfig({
246
+ url: env("APP_URL"),
247
+ allowedOrigins: ["https://admin.example.com"],
248
+ });
249
+ ```
250
+
251
+ Origins are compared exactly: no wildcards and no suffix matching, because
252
+ `endsWith(".example.com")` also matches `evil-example.com`.
253
+
254
+ > **Warning** — an app with the wrong origin configured renders every page correctly and refuses every action. There is no 500, nothing in the logs, and a status-code health check passes. The only symptom is that buttons do nothing.
255
+
256
+ ### Never gate the transport path
257
+
258
+ **Browsers do not attach basic-auth credentials to a WebSocket handshake.** An HTTP auth
259
+ gate over a whole site — a pre-launch gate, an internal tool — therefore gates the
260
+ transport, and the app degrades to slow HTTP fallback or stops working entirely.
261
+
262
+ Exempt the transport path:
263
+
264
+ ```caddyfile
265
+ # Caddyfile
266
+ your.app {
267
+ encode zstd gzip
268
+
269
+ @gated not path /__flow/* # browsers don't send basic-auth on a WS handshake
270
+ basic_auth @gated {
271
+ staging $2b$12$…
272
+ }
273
+
274
+ reverse_proxy 127.0.0.1:3002 {
275
+ flush_interval -1 # long-lived socket: don't buffer or reap it
276
+ }
277
+ }
278
+ ```
279
+
280
+ Be honest in your own config about the trade-off: action frames are then reachable without
281
+ the gate password. That is usually fine — a staging gate keeps a site out of search results
282
+ and away from passers-by, while your app's own login is what protects data, and that still
283
+ applies to every action arriving this way. But it is a decision, and the next person should
284
+ find it written down.
285
+
286
+ On a host that already serves other sites, append to the config rather than overwriting it,
287
+ validate, then reload — a syntax error takes down every app on the box:
288
+
289
+ ```bash
290
+ # on the server
291
+ caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
292
+ ```
293
+
294
+ ### Hardening the service
295
+
296
+ ```ini
297
+ # /etc/systemd/system/your-app.service
298
+ [Service]
299
+ User=app
300
+ WorkingDirectory=/opt/app
301
+ ExecStart=/opt/app/.bun/bin/bun zt serve --port=3002
302
+ Restart=always
303
+
304
+ NoNewPrivileges=true
305
+ ProtectSystem=strict
306
+ ProtectHome=read-only
307
+ PrivateTmp=true
308
+ ReadWritePaths=/opt/app/database /opt/app/storage
309
+ ```
310
+
311
+ Grant write access to the directories the app genuinely writes — its database and its
312
+ storage disk. Source, `node_modules` and `.env` have no business being writable by the
313
+ running process. With assets built at deploy time, `public/` does not need to be writable
314
+ either; if you would rather let the app build at boot, add it to `ReadWritePaths`.
315
+
316
+ Give each app its own copy of Bun under its own directory. A Bun installed as root at
317
+ `/usr/local/bin/bun` is usually a symlink into `/root/.bun/`, which a service user cannot
318
+ traverse — the error is `Permission denied`, not `not found` — and a runtime upgrade for
319
+ one app should not be able to break another.
320
+
321
+ ## Verifying a deploy
322
+
323
+ `bun zt doctor` runs every static check against a release: `APP_KEY` strength, the
324
+ transport origins, whether the schema has one source of truth, providers that were
325
+ configured but never registered.
326
+
327
+ ```bash
328
+ # in your project root, on the server
329
+ bun zt doctor
330
+ ```
331
+
332
+ Static checks run inside the process, and the expensive proxy failures are exactly the
333
+ ones that cannot be seen from there. `--url` probes the deployed app from the outside,
334
+ through the real proxy, with a real handshake and a real `Origin`:
335
+
336
+ ```bash
337
+ # in your project root
338
+ bun zt doctor --url=https://your.app
339
+ ```
340
+
341
+ It reports each registered WebSocket path, and reads the status the server actually
342
+ returned: `101` means a browser can open the socket; `403` is the origin guard; `401` is an
343
+ auth gate over the transport; `404` usually means the proxy is not forwarding the path.
344
+
345
+ To check it by hand, pass `--http1.1`. curl over TLS negotiates HTTP/2, where
346
+ `Connection: Upgrade` is meaningless, and you get a `404` that reads exactly like a broken
347
+ route on a server that is working perfectly:
348
+
349
+ ```bash
350
+ curl -s -o /dev/null -w '%{http_code}\n' --http1.1 \
351
+ -H 'Origin: https://your.app' -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
352
+ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
353
+ https://your.app/__flow/ws # expect 101
354
+ ```
355
+
356
+ Note that a plain `curl` against an action endpoint proves less than it appears to: a
357
+ request with **no** `Origin` header is deliberately allowed through, because native and CLI
358
+ clients do not send one. The check only bites when an origin is declared — which is what a
359
+ browser always does, and what `--url` reproduces.
360
+
361
+ ### Pre-launch checklist
362
+
363
+ Everything here is something that can pass a green test suite and still break in
364
+ production.
365
+
366
+ - `APP_URL` is the public URL, and `bun zt doctor` reports it under Transport origins
367
+ - `bun zt doctor --url=…` returns `101` for every transport path
368
+ - The transport path is exempt from any proxy-level auth gate
369
+ - A **browser** — not curl — has clicked a real action against the deployed site
370
+ - The service survives `systemctl restart` and comes back listening
371
+ - The app's port doesn't collide with anything already on the host
372
+ - The service user can execute its own Bun binary
373
+ - `APP_KEY` was generated on the server, not carried from a laptop
374
+ - No development database was copied to the server
375
+ - Backups actually exit `0` — run the unit once and check
376
+
377
+ ## Compile to a single binary
378
+
379
+ Bun can compile the app — runtime, dependencies, and your code — into one
380
+ self-contained executable, so the deploy artifact needs no `bun install` or
381
+ `node_modules`:
382
+
383
+ ```bash
384
+ # in your project root
385
+ bun zt compile --outfile=zerotal-app
386
+ ./zerotal-app serve --port=3000
387
+ ```
388
+
389
+ This produces a portable binary ideal for slim containers and edge hosts. Ship your
390
+ `config/`, migrations, and built `public/` assets alongside it, and provide the same
391
+ environment variables at runtime.
392
+
393
+ ## A minimal Dockerfile
394
+
395
+ ```dockerfile
396
+ # Dockerfile
397
+ FROM oven/bun:1 AS base
398
+ WORKDIR /app
399
+
400
+ # Install deps first for better layer caching
401
+ COPY package.json bun.lock ./
402
+ RUN bun install --frozen-lockfile
403
+
404
+ # App source
405
+ COPY . .
406
+ RUN bun zt assets:build # omit if you have no frontend bundle
407
+
408
+ ENV APP_ENV=production
409
+ EXPOSE 3000
410
+
411
+ # Run migrations then serve (use your platform's release step for migrate in real setups)
412
+ CMD bun zt migrate && bun zt serve --port=3000
413
+ ```
414
+
415
+ Run the worker as a **second** container/service from the same image with the command
416
+ `bun zt worker`.
417
+
418
+ On a server with no Node installed, `bun install` can fail on a transitive package whose
419
+ `postinstall` shells out to `node`. `--ignore-scripts` resolves it, but check what you are
420
+ skipping first:
421
+
422
+ ```bash
423
+ # every package with an install script, before you skip them all
424
+ for p in node_modules/*/package.json node_modules/@*/*/package.json; do
425
+ grep -l '"\(post\|pre\)\?install":' "$p"
426
+ done
427
+ ```
428
+
429
+ ## Next steps
430
+
431
+ - [Configuration](/docs/config-system) — environment variables and config files.
432
+ - [Migrations](/docs/migrations) — schema changes shipped with each release.
433
+ - [Queue](/docs/queue) — what the worker process runs in the background.
434
+ - [Health](/docs/health) — the health endpoint to wire into your platform's checks.