zpack.yaml manifest plus scripts: it declares a base image, per-instance volumes, environment, supervised services, computed outputs, and verbs. You zomg install a pack once, then instantiate it as many boxes as you like — zomg pg create db1, zomg pg create db2 — each a normal box tagged with the pack it came from.
Everything a pack does maps onto a primitive that already exists and evolves on its own: base builds, data volumes, the service supervisor, snapshots and bases. A pack is the join point, not a new runtime. Every section of the manifest is optional, and deleting any one section leaves a valid pack. App packs add a dev: overlay and a requires: edge and get a full dev → deploy → rollback loop on those same primitives — no Dockerfile, no pipeline definition.
The manifest
A pack directory containszpack.yaml and its scripts. Relative script paths resolve against the pack directory, which is materialized read-only inside every instance at /box/pack/<name>. pg is the running example throughout this page:
Sections
| Section | What it declares |
|---|---|
name | The pack name (a-z0-9-), validated against builtin CLI names. / is forbidden — it is reserved for a future owner/name tap syntax. |
description | Free text shown by zomg pack list/show. |
instances | many (default), single, or none (a base-only or CLI-only pack). |
base | A parent base and a build script; installing the pack builds it server-side and aliases it zpack-<name>. Omit it to instantiate straight from box-base. |
volumes | Named volumes. { path } is a per-instance volume created as {box}-<key>; { global: <name>, path } attaches an existing deployment-global volume instead. |
env | One values store per instance. A value is a literal string or a provenance spec: { generate: <kind> }, { required: true }, and/or { secret: true }. |
services | Supervised processes, each with a run (pack script) or command (shell string), a port, and optional user, cwd, ready, stop, restart, and publish. |
outputs | A key → template map computed on read from live state (never stored). Secrets flow through — an output that references a secret env value is itself redacted. |
verbs | Named invocations: a run (pack script) or exec (interactive shell string), with optional args, stdin, and user. Reserved names are the lifecycle hooks. |
views | Declared UI surfaces (- terminal: {…} / - page: {…}) that decorate a service or verb. |
requires | A KEY: pack map of edges the instance must be given at create, enforced: each alias must arrive via --env-from from an instance of the named pack. A literal --env never counts and never records an edge. |
dev | The dev overlay: a source cloned to /work/src, a build command, and per-service command overrides. Dev is an overlay — deploy-truth (services.*.run/command) is the base. See Dev and deploy. |
deploy | An exclude list of absolute in-root paths scrubbed from the release snapshot at deploy time, on top of the platform’s own secret scrub. |
views parse and validate and feed GET /v1/apps — the endpoint behind the dashboard’s Apps nav: a page view becomes an app entry with its published URL, a terminal view carries its verb. Everything else on this page, including requires, dev, and deploy, executes today; see Verbs for the two reserved hooks that are still unwired.env and outputs values: {box}, {host} (the platform-owned {box}.{project} reachability name — never assemble DNS yourself), {env.KEY}, and {volumes.KEY.path}. {publish.url} is available only when exactly one service declares a publish, and it renders lifecycle-correct: standard instances and deploy generations render the stable publish-name URL, dev instances render their own gated <box>.zomg.ai URL — so a BASE_URL: "{publish.url}" env literal is right from first boot in both worlds. In outputs it is computed from live publish state, and outputs that reference it are omitted while the instance is unpublished.
How env reaches processes
Instance env lands in/etc/boxes-env as export KEY='VALUE' lines with the value single-quoted, so $(…), backticks, and $VAR inside a value are inert — a value round-trips byte-for-byte through login shells, verbs, and services, and never executes. Pod boot is the file’s only writer: it is regenerated from the Deployment env on every pod start, and any env change (zomg env refresh, a verb writing $ZOMG_ENV_OUT) restarts the box to take effect. Supervised services source /etc/boxes-env after /etc/profile.d, so a manifest value like LD_PRELOAD: "" wins over the base’s eatmydata preload inside the service process — that empty string is how pg keeps fsync real for postgres while the rest of the box stays fast.
Install and pins
zomg install does the fetch client-side (a local directory or a GitHub owner/repo[#subdir]), tars the directory, and uploads it — the server never runs git. The content is stored per-revision and the pack is pinned to that rev; each instance remembers the rev it was created from, so a later zomg pack update only changes the default for new instances. Old instances keep resolving verbs, outputs, and scripts through their own rev.
base, installing it triggers a server-side base build: the build hashes the base script against the parent base version, spins up a throwaway builder box, runs the script, snapshots the result as a version of the base alias zpack-<name>, and deletes the builder. The hash is a cache key, so a second install of unchanged content is a no-op (already built). Build logs stream like service logs, and every client — CLI, SDK, or dash — triggers builds identically.
Instances are boxes
An instance is a normal box, labeledzpack=<name> and annotated with its pinned rev. There is no instance-group concept: multi-box topologies are composition via edges (an app instance plus a pg instance plus an --env-from edge). Creating an instance runs a fixed orchestration:
Volumes
Each declared volume becomes a box-owned volume named
{box}-<key> (or attaches a named deployment-global volume). Box-owned volumes are deleted with the instance; global volumes outlive it.Environment
The server renders literal values, generates
generate: values once, and merges --env and --env-from under a one-source-per-key rule — supplying a key from two sources is a create-time error. A required env key must be satisfied by --env or --env-from, and every requires: alias must be satisfied by --env-from from an instance of the named pack.Pack content
The pinned-rev pack directory is copied read-only into the root at
/box/pack/<name>, so it forks and snapshots with the root. Dev instances then clone dev.source to /work/src and run dev.build in place.Setup, release, and services
The reserved
setup verb runs once, then release (migrations) if declared — both fatal on a nonzero exit — then the manifest’s services start under the supervisor in name order, each gated on its ready check before the next starts (so matrix’s postgres is ready before synapse spawns). On box stop/resume the supervisor’s boot reconcile restarts them unordered: a service that depends on a sibling must converge via restart: always crash-backoff (the default).generate: of hex32, hex64, base64-32, or uuid) are created once per instance. Marking a value secret: true redacts it from events and flags it in outputs.
Outputs and connect edges
Outputs are computed on read from the instance’s pinned manifest and its live environment — never materialized:The secret-display rule
One rule governs every human surface: a value derived from asecret: true env key is redacted by default in human-readable output — shown as ******** (secret) — and revealed only two ways: --json (the programmatic retrieval surface machines and scripts read) or an explicit --show-secrets on the human command. The post-create echo, zomg outputs, and a service’s reproducible commands.start hint all follow it (a --secret-env KEY=VALUE on zomg service start marks a value so its start hint reads --env KEY=********). Events never carry values at all — verb args that contain a live secret are redacted to ***, and env changes are logged by key name only — so the event log is safe by construction regardless of this rule.
Wiring one instance into another is --env-from, which injects the source’s outputs as ALIAS_<KEY> env values and records a queryable edge {consumer, source, alias, who, when}:
zomg env refresh, which re-resolves the box’s edges and patches its Deployment env. Because a box runs one pod under the Recreate strategy, that patch restarts the box — the old pod stops, then the new one starts and regenerates /etc/boxes-env with the fresh values (pod boot is that file’s only writer). The response says whether the box was restarted. The consumer does not have to be a pack itself — a plain zomg create app --env-from db1:DATABASE records the edge too, and a manifest’s requires: is satisfied only by such an edge, never by a literal --env.
Verbs
A verb is the one invocation concept behind the CLI, MCP, and dash.zomg <pack> <verb> [args…] dispatches to an installed pack; unknown subcommands resolve against the installed-pack registry, and builtin command names are reserved (a pack cannot take them).
Standard verbs are a single data-driven engine over the existing box and service APIs — the same for every pack:
| Verb | What it does |
|---|---|
create | Create an instance box (accepts --env, --env-from, and box config). |
list | List this pack’s instances (boxes tagged zpack=<name>). |
show | Show one instance’s fields. |
status | Print an instance’s status. |
logs | Tail an instance’s service log. |
stop | Stop an instance (tears the pod down). |
start | Start a stopped instance. |
delete | Delete an instance and its box-owned volumes. |
snapshot | Checkpoint the root (fires pre-snapshot first) and snapshot the per-instance volumes. |
fork | Fork an instance — clone volumes and copy state-coupled secrets. |
outputs | The instance’s computed outputs (same as zomg outputs). |
update / rebuild | Sugar for zomg pack update/rebuild <name>. |
dev | Create a dev instance: cloned dev.source, dev command overrides, gated publish under the instance’s own name. |
deploy | Bless a dev instance’s root as a new deploy generation and repoint the stable publish to it. |
rollback | Flip the stable publish back to the previous deploy generation. |
zomg pack update never changes an existing instance’s exec strings or verb set. A verb with exec: runs interactively over the console when stdout is a terminal; otherwise it runs through the verb route and returns stdout. A verb with run: executes a pack script. Verbs receive positional/flag args as $ZOMG_ARG_*, optional stdin, and can persist new env values by writing KEY=VALUE lines to $ZOMG_ENV_OUT (last write per key wins; the change is an event). Persisting env patches the instance’s Deployment template, which under the Recreate strategy restarts the instance pod — the verb response carries restarted: true and the CLI prints a note, and the new pod regenerates /etc/boxes-env.
Interactive exec: verbs are connect-style: because the session runs client-side over the console and never returns through the server, it cannot process $ZOMG_ENV_OUT. Use run: for any verb that must persist env. Interactive sessions are still attributed — an interactive event is recorded before the console opens (so zomg pg connect db1 is visible in zomg events, including from non-TTY CI). --project <p> selects the instance’s project on custom verbs just like the standard ones; flags the pack command owns (e.g. psql’s -c) are forwarded verbatim, and -- forwards the remainder literally.
| Reserved verb | When it fires |
|---|---|
setup | Once, at instance create (never on fork or deploy-stamp). |
pre-snapshot | Before a snapshot, and before a fork’s implicit snapshot, for crash consistency. |
install / uninstall | Pack scope, in a throwaway builder box (how a CLI-only pack copies a binary into a shared volume). |
on-fork | After a fork completes. |
on-publish | On a publish/unpublish change of a declared publish (a publish whose port a manifest service declares publish: on), with args: [url] — the new URL, or the empty string on unpublish. A failing hook warns and the publish stands. Deploy repoints of the same stable name never fire it — no URL change. |
release | At every instance create (dev included) and again in each fresh deploy generation, against its edges, before services start — migrations live here. Fatal: a nonzero exit aborts the create or rolls the deploy back. |
install and uninstall are reserved names the manifest may declare, but later milestones wire their execution. The others all fire today.Events
Every verb execution is an attributed event — the timestamp, who ran it, the pack and instance, the verb and its args, the outcome, and the duration — with any argument that contains a live secret value redacted to***:
<ts> <who> <pack>/<box> <verb> <args> <outcome> (<ms>ms). Events are the two-way human/agent observability substrate: zomg uses answers blast radius before a change, zomg events answers what happened after.
Fork
Forking a pack instance rides the normal box-fork path: it clones the box-owned volumes and copies the generated and required env values, so state-coupled secrets (likePGPASSWORD) travel with the state. The source’s pre-snapshot verb fires for crash consistency before its root is snapshotted, the connect edges are re-recorded with who = the person forking — a failure to record rolls the fork back, so zomg uses never lies by omission — and setup is skipped.
Dev and deploy
App packs add two blocks —requires: (the edge to a database pack) and dev: (the overlay) — and get a full dev → deploy → rollback loop with no Dockerfile and no pipeline definition. Both dev and deploy are ordinary instances of the same manifest; dev is an overlay, deploy is the base truth. This is the real zpacks/kutt manifest:
Dev instances
zomg kutt dev app --env-from db:DB creates an instance with the dev lifecycle (a pack must declare dev.source to run as one). After the normal create orchestration, the platform materializes dev.source at /work/src — a URL is a shallow git clone, a relative path copies out of the read-only pack content and restores writability — and runs dev.build there with the instance env. The dev tree lives in the root filesystem, never a volume: the root is exactly the thing deploy later blesses, and state that must never be blessed belongs in volumes or behind an edge.
Services then start with the dev.services command overrides where declared, falling back to deploy truth. Only the command is overridden — port, cwd, ready, and stop stay the same, so dev is never a parallel service set. With a watcher as the dev command (kutt’s npm run dev), edits to /work/src — by you or an agent over zomg exec — reflect live in the served page.
Dev publishes are deliberately different from the manifest’s declared publish:
- A dev instance publishes under its own
<box>.zomg.ai(or<box>-<name>when the manifest publishes several services). The stable manifest publish name belongs to deploy generations, so dev and deploy never collide. - A dev publish is always gated, even when the manifest says
public: true— a dev tree is never public.
release runs on dev-create too (before services, fatal): a dev instance needs its migrations just like a deploy generation does.
Deploy
zomg kutt deploy app blesses the dev instance’s exact root — a btrfs snapshot, not a re-run pipeline — and stamps a new generation from it. The order is load-bearing, and any failure before the final flip rolls the new generation back while the previous keeps serving:
Build in the dev instance
dev.build runs in /work/src so the root carries the built tree (kutt’s node_modules, miniflux’s compiled binary). Then pre-snapshot fires (fatal) for crash consistency.Snapshot, scrub, seal
The root is snapshotted writable, scrubbed, and sealed read-only as a new version of the hidden release base
zpack-<name>-release. The scrub removes every platform-injected secret — /etc/boxes-env (API tokens, generated env), root’s .ssh/.codex/.claude, shell history, verb spool files — plus the manifest’s deploy.exclude paths. It never touches .git, node_modules, or built binaries (those are the blessed artifacts), and it only ever mutates the snapshot, never the live dev root. Release bases are internal: hidden from zomg base list, refused by zomg create --base, still inspectable via base detail with kind: "release" provenance.Stamp the generation
A new box
<dev>-r<stamp> is created from the concrete release version with fresh per-generation volumes (deploy blesses the root, never volume state) and a composed env: the dev instance’s durable env copied forward (generated secrets like JWT_SECRET ride, so sessions persist across deploys), manifest literals re-rendered against the stable publish URL (this flips a {publish.url} BASE_URL from the dev URL), and --env-from values re-resolved fresh against the dev instance’s recorded edges — fatal, with the edges re-recorded for the generation. Identity (pinned rev, lifecycle=deploy, the rollback pointer) rides the create atomically.Release, services, ready gate
The
release verb runs in the new generation (setup deliberately does not — it is create-only), then services start with deploy-truth commands and no publish. The ready gate polls the supervisor until every service is running and, where it declares a ready check, ready — budgeted at the largest declared ready timeout plus slack, floored at three minutes. A generation that fails its ready gate never receives traffic.The flip
The stable publish repoints to the new generation in a single compare-and-swap ingress replace, carrying the gating annotation forward — atomic and zero-downtime. A first deploy publishes fresh instead. The previous generation is then stopped, not deleted: it is the rollback target, linked by the
zomg/zpack-rollback-to annotation.Rollback
zomg kutt rollback flips the stable publish back to the previous generation. The current owner must be one of this pack’s deploy generations with a recorded previous generation; that previous box is resumed if stopped (the supervisor’s resume reconciliation restarts its services), must pass the same ready gate deploy uses — against its own pinned-rev manifest — and only then does the same CAS repoint flip the publish back. The retired generation is stopped, not deleted. A previous generation that fails its ready checks never receives traffic; the current one keeps serving.
The database walkthrough: pg
The core loop, copy-pasteable, from installingpg to seeing the verb history:
The app walkthrough: miniflux and kutt
The acceptance demo end to end — install pg, dev an app against it, deploy, roll back. No Dockerfile, no YAML beyond the two pack manifests.zpacks/miniflux (Go, compiled) and zpacks/kutt (Node, interpreted) are the two shipped proofs; kutt shown here:
dev.build is go build, and a source edit reaches the served app only after the next deploy rebuilds and blesses the edited root — proof that deploy captures the exact root rather than re-running a pipeline. Its manifest also shows {publish.url} doing double duty: BASE_URL renders the gated dev URL on dev-create and the stable miniflux.zomg.ai URL on deploy.
Trust model
Installing a pack is the trust decision — the Homebrew model: you run what you install. A pack’s verbs run in-instance as the pack-declareduser, carrying the invoking caller’s identity for any API access; they never receive a deployment-admin credential implicitly. A pack’s only API surface is the services it publishes — packs never extend zomg-api routes, middleware, or auth, which stay core. There is no third-party marketplace yet; when one arrives, install-time review is the gate, not runtime sandboxing.
Related
Services
The supervisor that runs a pack’s declared processes.
Data volumes
Per-instance box-owned volumes and deployment-global volumes.
Snapshots and bases
The base builds and root snapshots that install and fork stand on.
Packs CLI
install, pack, outputs, uses, events, and env reference.