A zpack (pack) is a service described as data. Instead of a helper binary per database or app, a pack is a directory with a 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 contains zpack.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:
name: pg                          # a-z0-9-, validated against builtin CLI names; '/' reserved for taps
description: PostgreSQL 17 with pgvector
instances: many                   # many (default) | single | none

base:
  parent: box-base
  script: base-init.sh            # built server-side, cached, aliased zpack-pg

volumes:
  data: { path: /var/lib/postgresql/data }   # per-instance, named {box}-data

env:                              # one values store; provenance = literal | generate | required
  PGDATA: "{volumes.data.path}"                 # substitutions allowed; no duplicated facts
  PGPASSWORD: { generate: hex32, secret: true } # generated once per instance; copied on fork
  LD_PRELOAD: ""                                # durable DB: opt out of eatmydata

services:                         # every process is supervised; foreground exec
  postgres:
    run: scripts/run-postgres.sh  # run: pack script | command: shell string
    port: 5432
    user: postgres                # supervisor setuids before exec
    ready: { tcp: 5432 }          # or { http: /healthz, status: 200 }
    stop: { signal: INT, grace: 30 }
    # publish: { name: pg, public: false }   # publish-at-create; {publish.url} known before start

outputs:                          # computed on read from live state — never materialized
  host: "{host}"                  # {host} = {box}.{project}, platform-owned reachability
  port: "5432"
  user: postgres
  database: postgres
  password: "{env.PGPASSWORD}"    # inherits secret: redacted in events
  url: "postgres://postgres:{env.PGPASSWORD}@{host}:5432/postgres"

verbs:                            # one invocation concept; lifecycle = reserved names
  setup:        { run: scripts/setup.sh }        # reserved: at create only
  pre-snapshot: { run: scripts/checkpoint.sh }    # reserved: before snapshot and fork
  connect:      { exec: "psql -h 127.0.0.1 -U postgres -d postgres" }
  rotate:       { run: scripts/rotate.sh }        # persists new env via $ZOMG_ENV_OUT

views:
  - terminal: { title: psql, verb: connect }

Sections

SectionWhat it declares
nameThe pack name (a-z0-9-), validated against builtin CLI names. / is forbidden — it is reserved for a future owner/name tap syntax.
descriptionFree text shown by zomg pack list/show.
instancesmany (default), single, or none (a base-only or CLI-only pack).
baseA 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.
volumesNamed volumes. { path } is a per-instance volume created as {box}-<key>; { global: <name>, path } attaches an existing deployment-global volume instead.
envOne values store per instance. A value is a literal string or a provenance spec: { generate: <kind> }, { required: true }, and/or { secret: true }.
servicesSupervised processes, each with a run (pack script) or command (shell string), a port, and optional user, cwd, ready, stop, restart, and publish.
outputsA 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.
verbsNamed invocations: a run (pack script) or exec (interactive shell string), with optional args, stdin, and user. Reserved names are the lifecycle hooks.
viewsDeclared UI surfaces (- terminal: {…} / - page: {…}) that decorate a service or verb.
requiresA 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.
devThe 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.
deployAn 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.
Substitutions available in 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.
zomg install ./zpacks/pg          # local directory (the dev loop)
zomg install owner/repo#packs/pg  # a GitHub subdirectory

zomg pack list                    # installed packs
zomg pack show pg                 # revs + the pinned manifest
zomg pack update pg               # re-fetch and re-pin the default rev
zomg pack rebuild pg              # force a fresh base build
zomg pack remove pg               # refused while any instance exists
If a pack declares a 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, labeled zpack=<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:
1

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.
2

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.
3

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.
4

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).
Generated values (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:
zomg outputs db1                  # host = value, ... , password = ********  (secret)
zomg outputs db1 --show-secrets   # reveals the value in human output
zomg outputs db1 --json           # { "outputs": {…}, "secret_keys": ["password", …] }

The secret-display rule

One rule governs every human surface: a value derived from a secret: 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 create app --env-from db1:DATABASE
# app now has DATABASE_URL, DATABASE_HOST, DATABASE_PASSWORD, …

zomg uses db1               # who consumes db1 (blast radius) and what db1 uses
zomg env refresh app        # re-resolve edges after the source's values changed
There is no reconciler: staleness is fixed imperatively with 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:
VerbWhat it does
createCreate an instance box (accepts --env, --env-from, and box config).
listList this pack’s instances (boxes tagged zpack=<name>).
showShow one instance’s fields.
statusPrint an instance’s status.
logsTail an instance’s service log.
stopStop an instance (tears the pod down).
startStart a stopped instance.
deleteDelete an instance and its box-owned volumes.
snapshotCheckpoint the root (fires pre-snapshot first) and snapshot the per-instance volumes.
forkFork an instance — clone volumes and copy state-coupled secrets.
outputsThe instance’s computed outputs (same as zomg outputs).
update / rebuildSugar for zomg pack update/rebuild <name>.
devCreate a dev instance: cloned dev.source, dev command overrides, gated publish under the instance’s own name.
deployBless a dev instance’s root as a new deploy generation and repoint the stable publish to it.
rollbackFlip the stable publish back to the previous deploy generation.
Custom verbs are declared in the manifest and always dispatch through the target instance’s pinned pack rev — the rev it was created from — so 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.
zomg pg connect db1 -- -c "select 1"   # custom verb: psql through the box
zomg pg rotate db1                      # rotate the password; persists via $ZOMG_ENV_OUT
Reserved verbs are lifecycle hooks the platform calls at fixed points, never you directly — one concept for the whole lifecycle story. The manifest may declare any of them:
Reserved verbWhen it fires
setupOnce, at instance create (never on fork or deploy-stamp).
pre-snapshotBefore a snapshot, and before a fork’s implicit snapshot, for crash consistency.
install / uninstallPack scope, in a throwaway builder box (how a CLI-only pack copies a binary into a shared volume).
on-forkAfter a fork completes.
on-publishOn 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.
releaseAt 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 ***:
zomg events                         # latest verb executions
zomg events --pack pg               # only pg instances
zomg events --box db1 --json        # one instance, raw payload
Each line reads <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 (like PGPASSWORD) 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.
zomg pg fork db1 db2                # same data, same password, fresh instance

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:
name: kutt
description: Kutt URL shortener (Node + PostgreSQL)
base:
  parent: dev-base
  script: base-init.sh
requires: { DB: pg }                  # create/dev without --env-from DB:<pg instance> fails
env:
  JWT_SECRET: { generate: hex64, secret: true }
  DB_CLIENT: pg
  PORT: "3000"
services:
  web:
    command: 'DB_NAME="$DB_DATABASE" node server/server.js --production'   # deploy truth
    port: 3000
    cwd: /work/src
    ready: { http: /, status: 200, port: 3000 }
    publish: { name: kutt }           # the stable identity across generations
outputs:
  url: "{publish.url}"
dev:
  source: https://github.com/thedevs-network/kutt
  build: npm install
  services:
    web: { command: 'DB_NAME="$DB_DATABASE" npm run dev' }   # dev override: the watcher
verbs:
  release: { exec: "sh -c 'cd /work/src && DB_NAME=\"$DB_DATABASE\" npm run migrate'" }

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:
1

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.
2

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.
3

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.
4

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.
5

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.
Deploy refuses to repoint a publish it does not own — if the stable host is held by another project or by a box that is not an instance of this pack, the deploy fails before a generation is stamped. The publish name is the stable identity across generations and revs; generations come and go behind it. A failed deploy deliberately leaves its release-base version in place: it is the alias latest, and the next deploy supersedes it.

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.
zomg kutt deploy app       # app-r20260706123000 now serves kutt.zomg.ai
zomg kutt rollback         # flips kutt.zomg.ai back to the previous generation

The database walkthrough: pg

The core loop, copy-pasteable, from installing pg to seeing the verb history:
# 1. Install the pack (builds the pg base server-side; a second install is cached).
zomg install ./zpacks/pg

# 2. Create an instance — supervised postgres with a generated password.
zomg pg create db1

# 3. Connect interactively (a custom verb; psql through the box).
zomg pg connect db1

# 4. Wire a consumer to it — DATABASE_URL, DATABASE_HOST, … are injected and an edge recorded.
zomg create app --env-from db1:DATABASE

# 5. Inspect the edge (blast radius) and the computed outputs.
zomg uses db1               # app <- db1 as DATABASE (you, <when>)
zomg outputs db1            # host/port/user/database/url; password = ********  (secret)

# 6. Fork the instance — clones the data and copies the password.
zomg pg fork db1 db2

# 7. See every verb execution, attributed and secret-redacted.
zomg events --pack pg

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:
# 1. Install the database pack and the app pack (server-side base builds).
zomg install ./zpacks/pg
zomg install ./zpacks/kutt

# 2. Create the database the app requires.
zomg pg create db

# 3. Dev instance wired to it — requires: { DB: pg } is enforced at create.
zomg kutt dev app --env-from db:DB
# Dev instance app: https://app.zomg.ai (gated)
# -> source cloned to /work/src, `npm install` ran, release migrations applied,
#    `npm run dev` supervised. Edit /work/src and the watcher reflects it live.

# 4. Deploy: build, snapshot+scrub, stamp, migrate, ready gate, repoint.
zomg kutt deploy app
# Deployed kutt: app-r20260706123000
# URL: https://kutt.zomg.ai
# Previous: none (first deploy)

# 5. Iterate: edit /work/src in the dev instance, deploy again.
zomg kutt deploy app
# Previous: app-r20260706123000 (stopped - 'zomg kutt rollback' restores it)

# 6. Regret it: flip the stable publish back.
zomg kutt rollback
# Rolled back kutt: app-r20260706141500 -> app-r20260706123000

# 7. Every step is attributed.
zomg events --pack kutt
Miniflux is the same loop with the compiled twist: 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-declared user, 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.

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.