Storage Migrations
As your extension evolves, the shape of data stored in chrome.storage changes — keys get renamed, formats change, old data needs to be transformed. Without a migration system, users upgrading from an old version can end up with broken or stale state.
runStorageMigrations() in @repo/browser-utils implements a numbered migration list. The background service worker runs it on every startup. Each migration checks whether it has already been applied (via a stored version number) and runs only if needed.
How It Works
- The background worker defines a
STORAGE_MIGRATIONSarray with{ version, migrate }objects - On startup,
runStorageMigrations(STORAGE_MIGRATIONS)reads the current schema version from storage - Any migration with
versiongreater than the current version runs in order - After each migration, the new version is written to storage
- Migrations are idempotent — they run once per version
Adding a Migration
Migrations live in apps/background/src/index.ts. Each migration has:
- version: A number. Use incrementing integers (1, 2, 3, …)
- migrate: An async function. Use
getStorageValueandsetStorageValuefrom@repo/browser-utils
Example:
import { getStorageValue, setStorageValue, type StorageMigration } from '@repo/browser-utils';
const STORAGE_MIGRATIONS: StorageMigration[] = [
{
version: 1,
migrate: async () => {
const installReason = await getStorageValue<string>('installReason');
if (installReason === null) {
await setStorageValue('installReason', 'unknown');
}
},
},
{
version: 2,
migrate: async () => {
// Rename key: oldKey -> newKey
const old = await getStorageValue<string>('oldKey');
if (old !== null) {
await setStorageValue('newKey', old);
await removeStorageValue('oldKey');
}
},
},
];
const storageReady = runStorageMigrations(STORAGE_MIGRATIONS);
Sync vs Local
By default, migrations run against chrome.storage.local. To migrate sync storage, pass the area name:
runStorageMigrations(MIGRATIONS, 'sync');
Best Practices
- Never remove a migration — old installs may not have run it yet. Add new migrations with higher version numbers
- Keep migrations small — one logical change per migration
- Await storageReady — Background message handlers that read storage should
await storageReadybefore accessing migrated data