Versioned SQLite migrations for Bun, Node, and Deno — usable as a library or a CLI.
litevolve reads a directory of numbered SQL files ({version}_name.sql, {version}_name.down.sql, optional {version}_name.seed.sql) and applies them up or down against a SQLite database to reach a target schema version. Each step runs in a single BEGIN IMMEDIATE transaction so a failed seed rolls back its schema change too. The current schema version is tracked in SQLite's native PRAGMA user_version; a sticky init_seeds flag is recorded in an internal _db_meta table so seed behavior stays consistent across subsequent upgrades.
litevolve is still in embrional phase.
It will be available as:
npmpackage for use on Bun, Node, and Deno- executable for several targets (Darwin, Linux), architectures (x64, arm64), and platforms (glibc, musl)
- executable for Bun, Node, and Deno ecosystems
- docker image
The following instructions might not work since the integration is missing.
# Bun
bun add litevolve-bun
# Node: npm / pnpm / yarn
npm install litevolve-node
pnpm add litevolve-node
yarn add litevolve-node
# Deno (JSR) Not available yet
deno add jsr:@litevolve-deno/litevolve-deno
# Homebrew (CLI only) Not available yet
brew install litevolveNode caveat:
litevolve-nodeuses the built-innode:sqlite(DatabaseSync), added in Node 22.5.0 and still marked experimental (Stability 1.2 — Release Candidate). It emits anExperimentalWarningand its API may shift in a minor/patch release. Requiresnode >= 22.5.
// Import from the runtime-specific package: litevolve-bun or litevolve-node.
import { migrate_db } from "litevolve-bun"
// Apply migrations up (or down) to reach version 2.
// Returns the open Bun Database handle.
const db = migrate_db(
2, // apply_version: target schema version
"./migrations", // migrations_path: directory holding the .sql files
"./data/birds.db", // db_path: SQLite file (or ":memory:")
true, // init_seeds: only honored on a fresh DB at v0
)The signature lives at src/migrate.ts:172 and the error type at src/migration_error.ts:1.
The CLI takes the same four inputs as named flags:
litevolve \
--apply_version=2 \
--db_path=./data/birds.db \
--migrations_path=./migrations \
--init_seedsRuntime-equivalent invocations:
bunx litevolve --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations
npx litevolve --apply_version=2 --db_path=./data/birds.db --migrations_path=./migrations
deno run --allow-read --allow-write npm:litevolve \
--apply_version=2 --db_path=./data/birds.db --migrations_path=./migrationsTo run migrations during a Docker build without installing litevolve's runtime in your image, copy the binary from the official image in a multi-stage build:
FROM litevolve:latest AS migrator
FROM debian:bookworm-slim
COPY --from=migrator /usr/local/bin/litevolve /usr/local/bin/litevolve
COPY ./migrations /migrations
RUN litevolve --apply_version=3 --db_path=./data/app.db --migrations_path=/migrationsFor Alpine-based images, use the musl-linked variant:
FROM litevolve:musl AS migratorThis pattern is suited for baking a pre-seeded read-only SQLite file into an image. For runtime migrations against a writable volume, run litevolve at container startup instead.
Files in the migrations directory are validated by a strict regex:
0*[1-9][0-9]*_([a-z]|[A-Z]|_)+\.(sql|seed\.sql|down\.sql)
Breakdown:
0*— optional leading zeros for zero-padding (padding is not required).[1-9][0-9]*— the numeric version: a non-zero leading digit followed by any digits._— separator.([a-z]|[A-Z]|_)+— a[a-zA-Z_]+description.\.(sql|seed\.sql|down\.sql)— one of three extensions.
| Extension | Direction | When applied |
|---|---|---|
.sql |
up | when current_version < N ≤ target |
.down.sql |
down | when target < N ≤ current_version |
.seed.sql |
up + init_seeds | optional, same transaction as .sql |
Files that do not match the regex are silently skipped — keep auxiliary files (notes, fixtures, sub-directories) out of the migrations directory or they won't be picked up.
Sort order is numeric after stripping leading zeros, not lexicographic. 0999_x.sql sorts before 01000_y.sql because parseInt("0999") is 999 and parseInt("01000") is 1000. Padding is optional and its width can vary across migrations without breaking the order: 1_…, 0042_…, 0999_…, 01000_… all sort correctly together, and an unpadded 42_… sorts identically to 0042_….
Valid examples: 0001_create_initial_schema.sql, 1234_create_users_table.sql, 0042_add_users_language_column.down.sql, 01000_split_audit_log.seed.sql.
Invalid: 0000_foo.sql (no non-zero digit), 0_foo.sql (version 0), 0001-foo.sql (hyphen not allowed), 0001_foo.txt (wrong extension).
Notes about the parser (see src/migrate.ts:48):
- Line comments
-- …are stripped - Down migrations never apply seeds. Each
.down.sqlis responsible for its own data cleanup before dropping columns or tables
init_seeds is sticky: it is only honored when the database is at version 0 (fresh or fully rolled back). The chosen value is recorded in _db_meta and reused for every subsequent up-migration on the same database. Passing --init_seeds to a partially-migrated DB is silently ignored — this guarantees that a database either consistently has its seed rows or consistently does not. See the behavior contract in src/migrate.test.ts (the init_seeds_* tests).
The migrations/working/ directory in this repository ships a runnable three-version example modelling a bird-observation system (the sibling migrations/broken/ holds an intentionally-invalid migration used only by the test suite):
-
v1 (
0001_create_initial_schema.sql) — minimal core, four tables:observation_sites (id, name)birders (id, name, joined_at)time_slots (id, site_id, starts_at, ends_at, reserved)sightings (id, birder_id, site_id, species_common_name, observed_at, status)
Optional seed populates 3 sites, 8 birders (Alice Johnson, Bob Smith, …), 32 two-hour observation windows, and 3 sightings (
pending/verified/rejected). -
v2 (
0002_expand_schema.sql) — adds richer metadata viaALTER TABLE ADD COLUMNand creates two intake tables:observation_sitesgainslatitude,longitude,habitat_type,timezone.birdersgainsemail,skill_level,favorite_species,timezone.time_slotsgainsweather.sightingsgainsspecies_scientific_name,individual_count.- New tables
incoming_reports (id, source, raw_payload, received_at)andincoming_reports_archive (…, archived_at).
Optional seed back-fills coordinates, skill levels, scientific names, weather notes, and sets
timezone = 'America/New_York'for Central Park, Alice, and Bob. -
v3 (
0003_add_birder_mentors.sql) — addsmentor_birder_id TEXT REFERENCES birders(id)tobirders(a self-referential FK). Optional seed marks Alice as the mentor of Carol/Dan/Eve and Bob as the mentor of Frank/Grace.The down migration demonstrates the NULL-before-drop pattern for foreign-key removal:
-- 0003_add_birder_mentors.down.sql UPDATE birders SET mentor_birder_id = NULL; ALTER TABLE birders DROP COLUMN mentor_birder_id;
Clearing the FK values first is the right habit even when
DROP COLUMNwould also strip the inlineREFERENCESconstraint — it's the pattern you must use when removing a FK constraint while keeping the column, since SQLite has noALTER TABLE DROP CONSTRAINTand the alternative is aCREATE TABLE … / INSERT SELECT / DROP / RENAMEtable-rebuild that would otherwise copy stale references into the new table.
Drive it from the Makefile:
make migrate_seeds DB_PATH=./birds.db VERSION=2
sqlite3 ./birds.db "SELECT name, timezone FROM observation_sites;"Refer to Makefile for a comprehensive list of available helping commands.
- OSX is recommended for development
- if you have any experience contributing to this library under Linux please share your setup
litevolvebasic ecosystem is Bun- Makefile approach is opinionated (sorry)
- Use any editor but don't push any related configuration of it, keep it in your machine
- I currently use Helix editor