---
title: "Deployments & rollback"
description: "How a Guuey deployment works — immutable build snapshots, row-driven serving, zero-downtime rolling redeploys, byte-exact rollback, and the drift gate."
---

Every deploy of a Guuey agent — from [Studio](/studio/), `guuey deploy`, or
[`guuey agent apply`](/agents-as-code/) — produces a **build**: an immutable
record that holds everything the agent runs from. This page explains what
that record contains, why the hash an apply hands back really is the hash of
what serves, and what the platform guarantees (and refuses to fake) when you
roll forward or back. For the commands and CI wiring, see
[Agents as code](/agents-as-code/); for what the pod itself looks like, see
[Concepts: hosting](/concepts-hosting/).

## The deployment snapshot

A build's payload is its **snapshot**: your `guuey.json`, re-serialized into
one canonical string with the system prompt inlined byte-exact (a
`{ "file": "prompts/system.md" }` reference is resolved on your side and the
file's bytes are embedded, so the snapshot is self-contained — no lookup back
into your repo). If your app uses Guuey's generative-UI binding, that entry
is injected before the snapshot freezes, so the pod receives it the same way
it receives everything else.

**Canonical form** is what makes hashes meaningful. The platform doesn't
store your bytes as-sent; it parses the document against the schema and
re-serializes it, which emits keys in a fixed order. The same logical
document therefore always produces the same string — and the same sha256 —
regardless of how your editor formatted it. Two consequences you can rely
on:

- **Idempotency on the reconcile path.** An apply (`guuey agent apply`) or
  rollback whose assembled snapshot byte-matches the newest active build
  queues nothing and does not roll the pod — a no-op CI run is genuinely a
  no-op. `guuey deploy` and Studio are simple triggers: they always queue a
  new build, even for identical bytes.
- **A stable drift gate.** `deployedContentHash.snapshot` in the reconcile
  response hashes the exact persisted string; the `agentDef` and
  `systemPrompt` hashes cover your submitted bytes verbatim, so your
  pipeline can compare them against `sha256sum` of the reviewed files and
  fail red on any mismatch.

## Builds and their lifecycle

Builds are numbered **per app**, monotonically: each new deploy takes the
highest existing build number plus one. A build moves through `queued` →
(`building` for code-mode) → `deploying` → `live`; when a newer build goes
live, its predecessor is stamped `superseded`. `failed` and `superseded` are
terminal — no workload behind them, and they never block the next deploy.
The **newest active** build (queued, building, deploying, or live) is the
reference point: it is what "unchanged" and rollback compare against, and
during a rollout the old live row and the new in-flight row briefly coexist
until the handover completes.

`GET /v1/apps/:id/deployments` keeps the full history — every build's
status, plus the snapshot hash and provenance for builds that came through
`agent apply` or a rollback (Studio and `guuey deploy` builds carry `null`
for both) — and each build has its own
`…/deployments/:n/status` endpoint your pipeline can poll to liveness.

## Row-driven serving: the hash IS what serves

The deployment record is not a log of what happened — it is the **input** to
what runs. The pod boots by reading its snapshot from its build's record;
there is no side channel through which configuration reaches a serving
agent. The runtime image is pinned on the record by immutable image
**digest**, never a mutable tag. For no-code apps the platform advances that
digest when it ships a runtime update — auto-update is the default, and
`--runtime-auto-update off` pins the app to the runtime captured at its last
deploy — always by rewriting the record first, so the record still names
what runs ([runtime updates](/concepts-hosting/#runtime-updates)). Code-mode
images are per-build and change only with a new build.

That is why the snapshot hash in a reconcile or rollback response is a
guarantee rather than a receipt: nothing can change what serves without
writing a new build record with a new hash. If the hash you got back matches the bytes you
reviewed, the agent that answers your users is running those bytes — today
and until the next build goes live.

## Rolling redeploys

Agent pods are long-lived, so a redeploy is a **rolling update**, not a
restart: the new build's pod starts alongside the old one, and the old pod
is only taken out of service after the new one is ready. In-flight
conversations on the old pod are drained, not cut off.

"Ready" is a real check, not a liveness ping. Each pod exposes a readiness
check that reports not-ready when the pod cannot do its job — a required
colocated tool degraded, or the pod draining for shutdown. A pod that
receives its shutdown signal flips to not-ready immediately and stays that
way; within one probe window traffic stops routing to it, and any request
that still lands on it during the drain gets a structured `503` with a
`Retry-After` and succeeds on a live pod on retry (see
[Concepts: hosting](/concepts-hosting/#what-failure-looks-like)).

Combined with snapshot idempotency, this gives the operational property CI
depends on: **an apply only ever rolls the pod when the bytes changed**, and
when it does, users never see a gap.

## Rollback: byte-exact or refused

`guuey agent rollback --to <n>` re-serves a previous build's exact bytes as
a **new** build — a new number, a fresh rollout, but the pinned build's
persisted snapshot as its payload. The platform does not "rebuild from that
commit"; it re-serves the string that build's pod booted from.

The guarantee is strict: before queuing, the stored snapshot is run back
through the same canonical assembly a fresh deploy uses, and the result must
be **byte-identical** to what was stored. If it no longer round-trips — the
canonical form has moved since that build (a schema default was added, a
generative-UI binding the build pre-dated) — or the target is a code-mode
build or a record without a snapshot, the rollback answers
`409 ROLLBACK_NOT_EXACT` and names the remedy (`guuey agent apply` from that
commit). Guuey refuses to serve an approximation and call it a rollback.

One boundary to know today: a build whose snapshot pre-dates the app's
generative-UI binding — deployed in the window before the binding was
provisioned, typically a deploy fired right after app creation — is refused
with `ROLLBACK_NOT_EXACT`; re-apply from the repo instead. Studio and
`guuey deploy` builds otherwise round-trip, because every deploy path
persists the same canonical, binding-injected form.

Three more properties worth knowing:

- **Every gate re-runs.** A rollback passes the same checks as a fresh
  deploy — schema, lints, tier and size ceilings, build caps. Old bytes
  don't grandfather past current limits.
- **Build material only.** Rollback never touches the app's access policy
  (auth mode, allowed origins, guest access). If the old manifest declared
  different `app.access`, apply from that commit instead.
- **Provenance travels with the bytes.** The new record carries the pinned
  build's `repo`/`path`/`sha` verbatim — the bytes _are_ that commit's —
  plus a separate `rolledBackFrom` marker naming the build it re-served.

## Provenance and the drift gate

Provenance (`repo`, `path`, `sha`) is an **audit label, not a verified
fact**: it is asserted by the caller at apply time and read back on the live
build and every historical one. Guuey does not check out your repo to
confirm it. The _hashes_ are the truth — the drift gate exists so your
pipeline proves, cryptographically, that the persisted bytes are the
reviewed bytes, and provenance then tells a human which commit to look at.
Builds deployed outside CI (Studio, `guuey deploy`) simply carry no
provenance.

## Schema versions: refuse, never guess

`guuey.json` carries a root `schema` version (`"1"` today), bumped only when
the document shape changes in a way an older reader cannot interpret. The
CLI and the platform compare against the **same** constant, published in
`@guuey/config`, and both sides refuse rather than best-effort parse a
document from the future:

- The CLI stops **before any network call** with `SCHEMA_TOO_NEW` and tells
  you to upgrade `@guuey/cli`.
- The reconcile and deploy routes answer `400 SCHEMA_UNSUPPORTED` when the
  document is newer than the platform serves.

An **older** schema is accepted only through a real migration to the current
one; none exists yet (there has only been one version), so today the rule is
simply "equal, or refused". The point of the stance is that a document is
never silently reinterpreted: what a build means is fixed by its schema
version, which is exactly what makes byte-exact rollback a meaningful
promise.