# Releases & previews

> snoozestack preview runs your checkout in your own temporary copy of the project; snoozestack publish ships it to production as an immutable release; publish --release rolls back.

[HTML version](https://snoozestack.com/docs/releases/) · [Docs index](https://snoozestack.com/llms.txt)

Three commands. Make changes locally, run `snoozestack preview` to try them in a temporary copy of the project, run `snoozestack publish` to ship them. Nothing sits in between, and nothing assumes git.

| Command | What it does |
| --- | --- |
| `snoozestack preview` | runs your checkout in your own temporary copy of the project, at its own URL |
| `snoozestack publish` | cuts an immutable release from your checkout and deploys it to production |
| `snoozestack releases` | lists what publish has recorded; `publish --release <n>` deploys one again |

### Previews

A preview runs production's secret values with everything your checkout declares on top — its functions, schema, sites, storage, queues, schedules and `[auth]` (redirect URLs, providers, `confirm_signups`) — so what you test is what you will publish. `snoozestack logs runtime --preview` tails it: auth, functions and storage are one process, and that log is everything happening inside. It does not copy production's data: the database starts empty, or from what a `[preview.seed]` policy selects and anonymizes (below). It has its own users, its own storage, schedules that never auto-fire and queues with no dispatch trigger, and every email its runtime sends is captured in its outbox instead of delivered, so a sign-up flow can be walked end to end without a real inbox.

```bash title="terminal"
$ snoozestack preview
Creating preview-@ada of "acme"…
…
preview-@ada: building... ready

  https://preview-0f4a4c3e1c1e4bd29a3f1234567890ab-acme.snzzz.com
  expires in 30 minutes — running "snoozestack preview" again resets the clock
  sign in with  user@test.com / 1234  (seeded account, owner of the 212 seeded accounts rows)
  seeded from production under policy v1: accounts 212, orders 1,930; emptied 3
  schema plan: 2 changes against production's schema

Production is unchanged. When it checks out: snoozestack publish
```

Each person has one preview per project, named after their account — `preview-@<handle>`, or a hash of the account email when there is no handle — so a project's preview list says who is working on what. Running `snoozestack preview` again updates yours in place: same URL, same database, same users. It expires 30 minutes after it last came up, and every run resets the clock; at expiry its container, data, queues, schedule rows and secret overrides are removed.

```bash title="terminal"
snoozestack preview status    # URL, health, seed outcome, schema plan, expiry
snoozestack preview outbox    # the sign-up link it "sent"
snoozestack preview delete    # clean it up now
```

### Publishing

`snoozestack publish` goes straight to production. It uploads every artifact dark, records them as one **release** — an immutable package naming one version of every artifact, with a checksum per file — and deploys it in the same step. No preview is required, nothing needs approving, and whoever can publish changes production. Nothing is live until the last step, so a publish that fails part-way leaves production as it was.

```bash title="terminal"
$ snoozestack publish
Publishing "acme" to production…
…
Published "acme" — release r12 is production.
  schema: pointer moved
  runtime: pointer moved to 3f9a1c2e
  site www: live
  runtime: healthy
```

A release records artifact *ids*, never a rebuild: the runtime tree (source plus bundles), `schema.json`, each declared site's version, a sha256 per runtime file, the schema plan against what production served when it was published, what it dropped, its declarations — queues, triggers, webhooks, notification services, schedules, auth config and templates, each applied as a create or update, never a delete — and who published it. The portal recomputes the checksums from what it stored before it records anything.

There is no base check. Two people publishing from different checkouts overwrite each other: last write wins.

### Dropping columns and tables

Removing a column or a table from `snoozestack/schema.mjs` and publishing drops it. That is your call, not the platform's. But the edit that destroys production data can be one commented-out line, and `publish` is the only place it is ever seen, so it says what it is about to drop — with how much each holds in production right now — and asks:

```bash title="terminal"
$ snoozestack publish

  This release drops data:
    settings.outreach_paragraph   (text, 1,204 non-null rows)
    leads.legacy_source           (text, 0 non-null rows)

  Dropped data cannot be recovered by rolling back — an older release's
  code runs against the current schema.

  Continue? [y/N]
```

It never refuses: one keystroke proceeds, and `--yes` answers for a script. The release records what it dropped and `snoozestack releases` marks it.

### Rebuilding tables

A foreign key, a unique key, `.notNull()`, a default or a type changing on an existing table rebuilds it, copying every row across (see [Schema](https://snoozestack.com/docs/schema.md)). `publish` shows each rebuild with the rows it keeps, and asks only when a type change converts values already stored:

```bash title="terminal"
$ snoozestack publish

  This release rebuilds tables, copying every row across:
    lead_searches   1,204 rows preserved
                      +campaign_id
                      +campaign_id → campaigns.id
    leads           8,431 rows preserved
                      +campaign_id
                      -unique(owner_id, source, external_id)
                      +unique(owner_id, campaign_id, source, external_id)
                      +campaign_id → campaigns.id
```

When production's rows cannot take the new definition — NULLs under a new `.notNull()`, two rows sharing a new unique key, a reference to a parent that is gone — publish stops before uploading anything and says how many rows are in the way. Rolling back never rebuilds: an older release runs against the tables as they are.

### Releases and rollback

```bash title="terminal"
snoozestack releases                  # r12  live  2026-09-18…  ada@acme.test  13 runtime files…  ⚠ drops settings.outreach_paragraph
snoozestack releases show r12         # the full package: checksums, schema plan, drops, deployments
snoozestack releases deployments      # every publish and rollback: who, when, how production answered
snoozestack publish --release r11     # deploy r11 again — a rollback
```

`publish --release` deploys that exact recorded package — its artifacts by id, never rebuilt from your checkout. Pointed at a release older than the one live it is recorded as a rollback.

A rollback restores code, not data. Schema moves forward only: rolling back to a release that predates a drop gives you the old code running against a schema without the column, and nothing a later release added is dropped by rolling back past it. Restoring data is a separate, deliberate thing.

### Seeding a preview from production

By default a preview starts with an empty database. Declare a `[preview.seed]` policy in `snoozestack/project.toml` and it starts from a copy of production's data instead — reduced and anonymized before it serves a single request:

```bash title="snoozestack/project.toml"
[preview.seed]
version = 1
roots = ["accounts"]            # the rows that seed the preview…
include = ["plans"]             # …plus reference tables copied whole
account = "accounts.owner_id"   # optional: the test account owns these rows

[preview.seed.where]
accounts = "preview_seed = 1"   # which root rows — a column you set on the accounts you tag

[preview.seed.columns.accounts]
email = "email"                 # every text column of a kept table is classified
full_name = "person_name"
notes = "free_text"
plan = "none"                   # "none" is a classification, not an omission

[preview.seed.columns.plans]
name = "none"
```

| Kept | How |
| --- | --- |
| root rows | the rows each `where` predicate selects (all rows without one) |
| their closure | every row they reference, and every row that references them, transitively |
| `include` | every row of a reference table, without pulling in what points at it |
| everything else | emptied |

Classifications: `none`, `email`, `person_name`, `phone`, `address`, `free_text`, `url`, `ip`, `secret`. Each transform is one-way and shaped like what it replaces: an email is still an email, a unique column stays unique, and the same original maps to the same replacement within one seed so relationships across tables survive. Nothing recovers the original.

The policy fails closed. `snoozestack preview` refuses to upload a byte while a kept table has an unclassified text column; a gap found at seed time (a column added since, a predicate that does not run) empties the copy rather than serving production rows. The outcome — which tables were kept, how many rows dropped, what was transformed, or why the seed refused — is on `preview status`.

Every preview has a test account, `user@test.com` / `1234` — the one `snoozestack dev` seeds — and never production's users. Seeded rows are owned by production user ids that do not exist in the preview, so for an app that scopes rows to their owner, say which column holds that id and the test account takes it: `account = "accounts.owner_id"` in `[preview.seed]`. Signing in then lands on the seeded rows instead of an empty app. The account owns the rows of the id that owns the most of them — tag one owner's rows in the `where` predicate to make that exact. Only the id is reused: the account is minted fresh in the preview, with the test address and password.

**Why use it**

Preview, publish, and a list of what shipped. A preview named after you is a habit you can reach for in one command; a release is a numbered, checksummed package you can deploy again when something goes wrong; and the one thing that cannot be undone — dropping data — is shown to you, with counts, before it happens.

- **Try it on real-shaped data** — Your preview runs your checkout against production's configuration and, with a seed policy, anonymized production rows — at its own URL, with its own users.
- **Ship without ceremony** — One command from your working tree to production — no branch required, and nothing to wait on.
- **Roll back in one line** — publish --release deploys an earlier package's exact bytes; the database keeps moving forward.
