As your application evolves, the shape of your local data needs to evolve too. CL Sync’s migration system lets you write versioned, data-level transformations that run automatically during store.hydrate() when the persisted schema version lags behind the configured version. Migrations have full read/write access to records, collection states, outbox items, and metadata.
How migrations run
When hydrate() finds that the persisted schema version is less than schema.version, it runs each migration whose version number falls in the range (persistedVersion, configuredVersion], in ascending order. After all migrations complete, the new schema version is written to IndexedDB.
hydrate() throws a version mismatch error if the persisted schema version is newer than the configured one. This guards against data corruption when a user opens an older version of your app after upgrading. Always bump schema.version before shipping schema changes.
The migration context
Every migration receives a SyncMigrationContext that exposes typed read/write helpers for every table CL Sync manages:
Defining migrations
Declare an array of SyncMigration objects and pass them to createSyncStore. Each migration’s version is the version it migrates to.
Migration examples
Rename a field
Backfill a computed field
Prune stale outbox entries
Move collection state after rename
If you rename a collection, delete the old collection state so CL Sync doesn’t treat the stale key as valid:
Full example
Here’s a store configured with two sequential migrations:
If a user’s persisted version is 1, both migrations run in order on the next hydrate() call. If their persisted version is already 2, only the version-3 migration runs.
Migrations run synchronously inside hydrate(). Keep them fast — avoid network requests or heavy computation. If you need to fetch fresh data after a migration, do so after hydrate() resolves.