Migrations and Schema Evolution
Local-first migrations must preserve real user data without a server rollback button.
Migration Rules
- Version every durable format.
- Keep old schemas or sample fixtures in version control.
- Test every supported old version to latest.
- Make destructive migration an explicit product decision, not a fallback.
- Include backup-before-migrate for high-value data.
- Make derived indexes rebuildable.
Cross-Platform Map
| Store | Version mechanism | Test shape |
|---|---|---|
| Room | database version, exported schemas, Migration classes | old DB fixture -> migration -> schema validation -> data assertions |
| SQLite | PRAGMA user_version or app metadata table | copy old DB -> migration transaction -> integrity checks |
| IndexedDB | database version and onupgradeneeded/Dexie version() | old browser DB fixture -> upgrade -> object assertions |
| Tauri SQL | plugin migrations | install old app state -> launch new build -> verify DB |
| Files | manifest formatVersion | import old fixture -> rewrite or lazy upgrade -> validate checksum |
SQLite Migration Skeleton
BEGIN IMMEDIATE;
PRAGMA foreign_keys = OFF;
ALTER TABLE transactions ADD COLUMN category TEXT;
UPDATE app_meta SET value = '2' WHERE key = 'schema_version';
PRAGMA foreign_key_check;
PRAGMA foreign_keys = ON;
COMMIT;
Use a real migration runner in production. The important shape is one transaction, explicit version bump, and validation before success.
IndexedDB Sync Caveat
Synced browser data makes migrations harder because clients can run different app versions while sharing the same logical data. Prefer additive changes and backward-compatible readers. For large incompatible changes, consider export, offline migration, and importing into a new database.
Gotcha: A migration that passes on an empty database proves almost nothing.