norns

Migrations

Forward-only SQL files per feature, applied and tracked by the norns CLI.

bun run migrate create notes/add_pinned    # scaffold migrations/notes/<timestamp>_add_pinned.sql
bun run migrate up                         # apply pending migrations
bun run migrate status                     # list applied and pending

("migrate": "norns migrate" in package.json.)

#Layout

<project>/migrations/<feature>/<timestamp>_<slug>.sql

Migrations live at the project root, outside src/, one folder per feature. They are operational artefacts read by tooling, not application code, and keeping them out of src/lib keeps the bundler away from them. Files apply in file-name order across features, so the timestamp prefix gives a global chronology; the feature name breaks ties.

create does not check that the feature exists, so nested layouts such as src/lib/<group>/<feature>/ work too. A typo simply produces an orphan folder under migrations/ that is easy to spot.

#Tracking

Applied migrations are recorded in a norns_migrations table (id TEXT PRIMARY KEY, applied_at INTEGER). The id is <feature>/<file-without-.sql>. Each migration runs in a transaction together with its tracking insert, so a failing file leaves nothing half-applied.

#Target database

The CLI reads DATABASE_URL and supports SQLite only in v1: file:<path>, defaulting to file:./data/app.db. Any other scheme fails with a message pointing at the alternatives.

The SQLite backend is chosen at runtime: bun:sqlite under Bun (no native build, works on Alpine) and better-sqlite3 under Node, where the consumer app must have it installed.

DATABASE_URL=file:./data/notes.db bun run migrate up

#Cloudflare D1

norns migrate does not talk to D1. Point wrangler at the same folder and use its migration commands; see Cloudflare D1. Postgres and libSQL through the CLI are planned.