snoozestackdocs

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.

CommandWhat it does
snoozestack previewruns your checkout in your own temporary copy of the project, at its own URL
snoozestack publishcuts an immutable release from your checkout and deploys it to production
snoozestack releaseslists 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.

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.

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.

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:

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). publish shows each rebuild with the rows it keeps, and asks only when a type change converts values already stored:

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#

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:

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"
KeptHow
root rowsthe rows each where predicate selects (all rows without one)
their closureevery row they reference, and every row that references them, transitively
includeevery row of a reference table, without pulling in what points at it
everything elseemptied

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.