Locara

35 — Migrations

How an app’s stored data, manifest, and dependencies evolve safely across versions. Migrations are where local apps most often go wrong: a botched update can destroy years of accumulated user data with no cloud backup to fall back on.

What needs to migrate

Three independent migration tracks:

  1. SQLite schema — when the app’s db/schema.sql changes between versions.
  2. Manifest schema — when Locara’s manifest format itself evolves (locara/v1locara/v2).
  3. App-internal data — non-schema data the app stores (settings files, JSON blobs, embeddings cache).

Each has its own mechanism. All share the same hard rule: migrations must be safe under failure.

Hard rules

These apply to every migration:

  1. Atomic — the migration either fully succeeds or fully reverts. No half-states.
  2. Backed up — the previous data state is preserved before migration runs.
  3. Reversible within the backup window — if the new version is bad, the user can roll back.
  4. Tested — every migration runs in CI against representative fixtures before shipping.
  5. Documented — the changelog tells the user what’s changing and why.

SQLite schema migrations

How they’re authored

Each app version’s db/schema.sql is the source of truth for the desired schema. Migrations between versions live in db/migrations/:

db/
├── schema.sql                                  # the truth
└── migrations/
    ├── 0001_initial.sql                        # generated for the very first version
    ├── 0002_add_user_table.sql                 # generated for v0.2 → can be hand-edited
    └── 0003_add_embedding_index_for_search.sql # hand-written for v0.3

Generation flow

When the developer changes schema.sql:

$ locara db diff
Detected changes:
  + ADD COLUMN transcripts.duration_ms INTEGER
  + ADD INDEX idx_transcripts_duration

$ locara db migrate
Generated: db/migrations/0004_add_duration.sql
Review the migration and edit if needed before publishing.

The CLI generates SQL diffs for the obvious cases (add column with default, add index, add table). Non-trivial changes (column type changes, data backfills, table renames) prompt the developer to hand-edit the migration.

Hand-written migrations

For complex changes, the developer writes the migration directly:

-- 0005_split_name_to_first_last.sql
-- Forward migration
BEGIN TRANSACTION;

ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name TEXT;

UPDATE users SET
  first_name = substr(full_name, 1, instr(full_name, ' ') - 1),
  last_name = substr(full_name, instr(full_name, ' ') + 1)
WHERE full_name LIKE '% %';

UPDATE users SET first_name = full_name WHERE full_name NOT LIKE '% %';

ALTER TABLE users DROP COLUMN full_name;

COMMIT;

Hand-written migrations get extra review. locara verify flags them and reminds the developer to test.

Migration runtime behavior

When a user installs an updated version of an app:

1. Runtime detects schema mismatch (schema.sql in new version != current DB).
2. Runtime computes which migrations need to run (by version comparison).
3. Runtime backs up the current DB:
     ~/Library/Containers/<bundle-id>/Data/Documents/app.sqlite
   →  ~/Library/Containers/<bundle-id>/Data/Backups/2026-04-30T12:00:00.sqlite
4. Runtime runs migrations in order, each in its own transaction.
5. If any migration fails:
   a. Roll back the failed transaction.
   b. Restore from backup.
   c. Refuse the update; surface error to user.
6. If all succeed, app launches with new schema.
7. Backup retained for 30 days.

The user can manually trigger a rollback within the backup window from within the affected app’s settings (or from the optional Locara Manager utility, if installed).

Schema test invariants

Every published app must have:

  • A migration test that runs all migrations in order against an empty DB and confirms the resulting schema matches schema.sql.
  • A migration test that runs migrations on representative data and confirms data integrity (counts, key fields preserved).
  • For data-transforming migrations: a test that verifies a sample input produces the expected output.

locara test includes these by default; CI fails them.

Backup management

App data backups live at ~/Library/Containers/<bundle-id>/Data/Backups/:

  • One per migration event.
  • One per major version upgrade (kept longer).
  • Auto-pruned after 30 days unless the user pins them.

User can browse + restore from backups via the app’s own settings.

Total disk impact: typically <100MB per app. Apps with multi-GB data can configure a smaller retention.

Version skipping

Users may skip versions (e.g., v0.1 → v0.4 directly). Migrations run sequentially: 0001 → 0002 → 0003 → 0004. Developers should test version-skipping scenarios.

Manifest schema migrations

When this happens

The Locara manifest schema itself evolves: locara/v1 (today’s spec) might one day become locara/v2 with new required fields, restructured capabilities, etc.

Apps declare which schema version they target:

{ "schema": "locara/v1", ... }

Backward compatibility (the SQLite-style commitment)

A v1 app installed today must continue running in 5 years. The Locara runtime supports all historical schema versions indefinitely (the SQLite long-term commitment, applied here).

This means:

  • Adding new fields → never breaks existing apps (new fields are optional in old schemas).
  • Removing fields → very rare; old apps keep working with their declared field even if the new schema doesn’t recognize it.
  • Restructuring → done via opt-in migration; old apps keep their old schema until updated.

Opt-in migration

When locara/v2 ships, existing publishers can:

  1. Continue declaring locara/v1 — works fine, no action needed.
  2. Migrate to locara/v2 — read the migration guide, update the manifest, republish. Now you can use v2 features.

The CLI provides locara migrate-manifest that does the obvious mechanical conversions and surfaces ambiguous cases for the developer to resolve.

Schema deprecation policy

A schema version is supported indefinitely unless we discover a security flaw that requires deprecation. In that rare case:

  • 12-month deprecation period.
  • Migration guide published.
  • Affected publishers notified.
  • After deprecation: apps still install + run, but new versions of those apps must use the newer schema.

Actively-supported schema versions are listed in the runtime’s documentation.

App-internal data migrations

Apps may store data outside SQLite (config files, downloaded assets, embedding caches). These don’t get automatic migration support — the app handles them itself.

Best practices the framework encourages:

  • Store everything in SQLite where possible (it gets the migration story for free).
  • For non-SQLite data, version the directory structure: data/v1/, data/v2/. Apps detect and migrate on launch.
  • For derived data (caches, indexes), the safest answer is to delete and rebuild on version change.

The SDK’s db.export.* methods help apps produce portable backups of non-SQLite data alongside the SQLite snapshot.

Lockfile + dependency migrations

When an app updates and its locara.lock.json changes (new model versions, new SDK version):

  • Runtime fetches new model artifacts (verified by hash).
  • Old models with refcount > 0 stay in the cache; refcount 0 → eligible for cleanup.
  • Apps explicitly listing old models in capabilities continue to work; the runtime supports multiple versions side-by-side.

Multi-app migration coordination

For shared-folder IPC (see 03-capabilities.md): when one app updates and its shared-folder format changes, the partner app may not yet support the new format.

Policy: the format of shared folders is a stable contract between apps. Breaking changes require:

  1. New version of the format (e.g., transcripts-v2/ alongside transcripts/).
  2. Apps that consume the format continue reading the old format until they’re updated to consume the new one.
  3. Apps that produce the format can opt to write both formats in parallel during the transition.

Coordination between independent publishers is hard. The framework encourages stable formats with extension fields rather than breaking restructures.

Failure recovery

When a migration fails on the user’s machine:

  1. Surface the error clearly. Not “Update failed.” But “Transcribe v0.4 couldn’t migrate your database. Migration 0005_split_name failed at row 1247: text encoding error. Your data is intact. The previous version is still installed.”
  2. Restore from backup automatically.
  3. Offer to send diagnostic info. Not auto-uploaded — user must explicitly choose to share with the developer.
  4. Allow retry. User can retry the update if they think the issue was transient.
  5. Allow continued use of the previous version. The old version is still installed; nothing is destroyed.

This UX matters. Users will encounter migration failures; they should not feel like they’ve lost data.

What’s NOT supported in v1

  • Cross-publisher data migration. If you switch from app A to app B, B doesn’t automatically migrate A’s data. Each app handles its own data; export+import is the user’s responsibility.
  • Cloud-mediated migrations. No round-trip to a cloud service for data transformations. Everything happens locally.
  • Schema rollback after backup window. After 30 days, the old backup is gone. If you need to go back further, restore from Time Machine.

Open questions

  • (open) Should backups be encrypted with a key derived from user state? Probably yes for sensitive apps; defer to v2.
  • (open) How do we handle migrations for apps that have been uninstalled mid-process and reinstalled? Probably: detect, restore from backup, proceed.
  • (open) Migration tooling for @locara/sdk API breaking changes — automated codemods?

Cross-references