omg/docs

Deploy flow

From POST /v1/deploys to :slug.apps.omg.dev serving 200.

A "deploy" turns a durable version snapshot into a public app at :slug.apps.omg.dev. The editor sandbox does not become the production app. Control-plane publishes a runId by resolving its resultSnapshotId first, then infra forks a short-lived builder from that snapshot. The flow lives in apps/infra/internal/orchestrator/manager.go : Deploy().

Steps

1Reserve slug

InsertDeploy, status=building

2Resolve version snapshot

Control-plane passes snapshotId + runId

3Fork builder

Fresh VM from the files snapshot

4Build

vibes-build → dist/ + .vibes/

5Branch on manifest

static vs serverful path

  1. Reserve slugInsertDeploy with status=building. Concurrent first-deploys of the same slug see ErrSlugExists.
  2. Resolve version snapshot — control-plane waits for the version's end-of-turn files snapshot (runs.resultSnapshotId) and calls POST /v1/deploys with { snapshotId, slug, runId, async: true }. Publishing never snapshots or stops the live editor VM.
  3. Fork builder — fresh Firecracker VM forked from the version snapshot, used only to run the build. For files_only snapshots this restores the project tarball on top of the warm template.
  4. Build — runs vibes-build inside the builder. Emits dist/, .vibes/server.mjs, .vibes/data.db, .vibes/artifacts.json.
  5. Branch on manifest — read .vibes/artifacts.json. If static && !hasDb && functions==0 → static path; else serverful.

Static path (no VM at runtime)

Tar dist/ only inside the builder, read the tarball back via guest agent, walk it in-memory with archive/tar, and upload each entry to static-deploys/:slug/v:N/:path with per-file Cache-Control. No runtime VM. Finish with UpdateDeploy(Static=true, StaticVersion=N) — atomic flip.

Serverful path

  1. Tar dist/ + .vibes/ into a single artifact tarball, upload to project-tarballs/:artifactId/. Stop builder.
  2. Fork runtime from the production base or warm template (not from the version snapshot). If that base is absent locally, restore the warm rootfs from Tigris once per snapshot, then extract project.tar.gz on top.
  3. startDeployServer(slug, restoreV=N-1, replicateV=N):
    • litestream restore from deploys-db/:slug/v:N-1/data.db (skipped on first deploy).
    • litestream replicate -exec bun .vibes/server.mjs against deploys-db/:slug/v:N/data.db.
  4. WaitForDeployReady(forked.VMIP, 30s) — block until bun is accepting TCP on :3000.
  5. UpdateDeploy(SandboxID=forked.ID, ArtifactID=..., Version=N). Atomic flip.

Node loss and cold-start failover

The project artifact and warm rootfs are both durable in Tigris; local rootfs files are caches. The edge keeps a deploy on its assigned node while that node's heartbeat is healthy. A stale, draining, or storage-starved owner is replaced by a healthy peer, which restores the missing base and project artifact. The peer claims the deploy in sqld with a compare-and-swap, clearing the old sandbox pointer, then binds its new sandbox under the claimed node. This prevents two peers from cold-starting the same deploy or a late old-node write from stealing ownership back.

Atomic flip

Both paths converge on UpdateDeploy as the single source of truth for "what is the current version?". DeployProxy.ServeHTTP reads store.GetDeploy(slug) per request — there is no caching layer between sqld and the proxy decision. Edge-cache TTLs are short enough (60s for HTML) and v:N GC is deferred enough (90s) that the cached old HTML never out-lives its referenced assets.

Rollback

Pre-flip: anything failing in steps 5–8 cleans up the new sandbox and GCs the v:N Tigris prefix it was about to populate. Post-flip: a rollback flips the active static version by runId when retained; otherwise the same version can be rebuilt and published again. A failed deploy never modifies the live Deploy row.

Redeploy GC

After a successful flip:

  • Serverful — drain old sandbox (litestream sigterm flush), stop, GC v:N-1 Tigris prefix immediately.
  • Static — defer DeleteStaticVersion(N-1) by 90s so edge-cached old index.html (max-age=60s) doesn't 404 on its hashed assets during the transition.

The 90s defer must stay > the index.html max-age. serveStatic also falls back to v:N-1 on a hashed-asset miss, which is the lookup half of the same property.