Schema & migrations
A project's schema is a committed file: snoozestack/schema.mjs, plain JavaScript calling defineTable(). The runtime keeps the database matching it — locally on every edit, hosted at publish — and snoozestack migration generate records each change as a timestamped SQL file, so the folder of migrations is the readable history of every shape your data has had.
Your schema is part of your application, and it has to change in lockstep with the code that depends on it. The alternative — typing DDL into a SQL console whenever something needs adding — works exactly until the second environment or the second developer appears; then nobody can say what shape production is in. Here there is no SQL console to drift through: the file is the schema, it rides in the same commit as the code that needs it, and checking out an old revision gives you the schema that matched it.
The migration files are the honest ledger on top of that: reviewable SQL, one file per change, append-only. Once a migration has left your machine, don't edit it — fix a mistake by writing the next one.
Defining tables#
import { defineTable, schema } from "snoozestack-js"; export const notes = defineTable("notes", { id: schema.uuid().primaryKey(), user_id: schema.uuid().notNull(), body: schema.text().notNull(), created_at: schema.timestamp().notNull(),});The type vocabulary — text, int, bigint, real, boolean, timestamp, uuid, json, blob — is deliberately portable: every type has a defined mapping in more than one SQL engine, so the schema describes your data, not an engine. Under snoozestack dev, saving this file updates the local database in place — new tables and columns appear without losing the data you already have.
| Surface | Availability |
|---|---|
| Portal UI | Read-only: Database → Schema shows the shape the runtime built; changes come from this file |
| CLI | snoozestack schema build/push, snoozestack migration create/generate |
| SDK / HTTP | defineTable()/schema.* are exported by snoozestack-js — the schema file is ordinary code |
Recording a change#
snoozestack schema build # schema.mjs → snoozestack/schema.jsonsnoozestack migration generate add_notes # diffs against the last build, writes SQL# → snoozestack/migrations/20260828120000_add_notes.sqlgenerate is offline: it diffs a fresh build of schema.mjs against the last-built schema.json and writes the difference as SQL — no network call. Commit both the migration and the updated schema.json; the json is the baseline the next generate diffs against. Non-destructive changes (create/drop table, add/drop column, create/drop index) generate automatically. A column's type, nullability, or default changing on an existing table fails with a clear error instead of guessing — there is no single ALTER that covers those — so write that one by hand with snoozestack migration create.
Applying it#
You usually don't: the runtime applies your schema itself. Locally, snoozestack dev syncs the database on every edit; hosted, snoozestack publish (whose first step is schema push) applies the same declaration to the project. The migration files are the record of those changes — reviewable and re-playable — rather than a script you must remember to run. (snoozestack db push remains for older projects that manage their own database server, and migration generate takes an --engine flag to match.)
Seeding data#
Schema says what tables exist; a committed snoozestack/seed.mjs says what a fresh database starts with. It default-exports async ({ db }) => {} — the same db surface a function gets — and runs exactly once against a brand-new local database, so every clone of the repo starts with working data. snoozestack dev reset then snoozestack dev is the reseed path.
An agent's path#
An MCP-driving agent working on your repo edits schema.mjs and runs the same commands. One without a checkout gets the same guarantee through the MCP schema_define_table tool — one call that validates, diffs against the project's current schema, applies, and republishes it, server-side.