Releases & previews
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.
$ snoozestack previewCreating 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 publishEach 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.
snoozestack preview status # URL, health, seed outcome, schema plan, expirysnoozestack preview outbox # the sign-up link it "sent"snoozestack preview delete # clean it up nowPublishing#
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.
$ snoozestack publishPublishing "acme" to production……Published "acme" — release r12 is production. schema: pointer moved runtime: pointer moved to 3f9a1c2e site www: live runtime: healthyA 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:
$ 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). publish shows each rebuild with the rows it keeps, and asks only when a type change converts values already stored:
$ 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.idWhen 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#
snoozestack releases # r12 live 2026-09-18… ada@acme.test 13 runtime files… ⚠ drops settings.outreach_paragraphsnoozestack releases show r12 # the full package: checksums, schema plan, drops, deploymentssnoozestack releases deployments # every publish and rollback: who, when, how production answeredsnoozestack publish --release r11 # deploy r11 again — a rollbackpublish --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:
[preview.seed]version = 1roots = ["accounts"] # the rows that seed the preview…include = ["plans"] # …plus reference tables copied wholeaccount = "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 classifiedfull_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.
- 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.