Chrome Extension Storage Migration Strategies — Best Practices

7 min read

Storage Migration Strategies

Strategies for migrating extension storage data between versions.

Overview

When your extension evolves, the data schema stored in chrome.storage often needs to change. A robust migration system ensures user data is preserved and transformed correctly during updates.

Schema Versioning

Store a version number in storage and check it on startup:

const CURRENT_VERSION = 3;

async function initializeStorage() {
  const { schemaVersion = 0 } = await chrome.storage.local.get('schemaVersion');
  
  if (schemaVersion < CURRENT_VERSION) {
    await runMigrations(schemaVersion, CURRENT_VERSION);
  }
}

Sequential Migrations

Run migration functions in order (v1→v2, v2→v3, etc.):

const migrations = {
  1: migrateV1toV2,
  2: migrateV2toV3,
};

async function runMigrations(fromVersion, toVersion) {
  for (let v = fromVersion; v < toVersion; v++) {
    await migrations[v]();
    await chrome.storage.local.set({ schemaVersion: v + 1 });
  }
}

Idempotent Migrations

Design migrations to be safe if interrupted or re-run:

async function migrateV1toV2() {
  const data = await chrome.storage.local.get(null);
  
  // Check if already migrated
  if (data.settings?.theme) return;
  
  // Safe to run again even if partially complete
  const newSettings = { theme: data.theme || 'light' };
  await chrome.storage.local.set({ settings: newSettings });
}

Backup Before Migration

Copy data to a backup key before transforming:

async function backupBeforeMigration() {
  const allData = await chrome.storage.local.get(null);
  const backup = {
    data: allData,
    timestamp: Date.now(),
    version: allData.schemaVersion
  };
  await chrome.storage.local.set({ _backup: backup });
}

Lazy Migration

Transform data on read rather than all at once:

async function getSettings() {
  const { settings, _needsMigration } = await chrome.storage.local.get(['settings', '_needsMigration']);
  
  if (_needsMigration === 'v2') {
    return await migrateSettingsOnRead(settings);
  }
  return settings;
}

Common Migration Patterns

Field Additions

Merge new defaults with existing data:

async function addField() {
  const data = await chrome.storage.local.get('userPrefs');
  const defaults = { theme: 'light', notifications: true };
  await chrome.storage.local.set({ 
    userPrefs: { ...defaults, ...data.userPrefs } 
  });
}

Field Renames

Copy old key to new key, delete old:

async function renameField() {
  const { oldName } = await chrome.storage.local.get('oldName');
  if (oldName !== undefined) {
    await chrome.storage.local.set({ displayName: oldName });
    await chrome.storage.local.remove('oldName');
  }
}

Type Changes

Transform stored values to new format:

async function changeType() {
  const { count } = await chrome.storage.local.get('count');
  // String to number
  if (typeof count === 'string') {
    await chrome.storage.local.set({ count: parseInt(count, 10) });
  }
}

Collection Restructuring

Array to map, nested to flat:

async function restructureArray() {
  const { items } = await chrome.storage.local.get('items');
  const map = {};
  items.forEach((item, index) => {
    map[item.id] = item;
  });
  await chrome.storage.local.set({ items: map, _arrayMigrated: true });
}

Handling Missing Data

Don’t crash on unexpected schema:

function safeGet(data, path, defaultValue = null) {
  try {
    return path.split('.').reduce((obj, key) => obj?.[key], data) ?? defaultValue;
  } catch {
    return defaultValue;
  }
}

Migration Testing

Test with real user data samples:

// Test migration on sample data
const testCases = [
  { input: v1Data, expected: v2Data },
  { input: partialData, expected: mergedData },
];

testCases.forEach(({ input, expected }) => {
  const result = applyMigration(input);
  assert.deepEqual(result, expected);
});

Rollback Support

Store pre-migration backup for N versions:

async function createRollbackPoint() {
  const data = await chrome.storage.local.get(null);
  const backups = await chrome.storage.local.get('_backups') || {};
  
  backups._backups = [
    { data, timestamp: Date.now() },
    ...backups._backups?.slice(0, 2) // Keep last 3
  ];
  await chrome.storage.local.set(backups);
}

Logging

Record migration steps for debugging:

async function logMigration(from, to, status) {
  console.log(`[Migration] v${from} → v${to}: ${status}`);
  await chrome.storage.local.set({ 
    _migrationLog: `[${new Date().toISOString()}] v${from}→v${to}: ${status}` 
  });
}

Async Migrations

Handle large data sets without blocking:

async function migrateLargeDataset() {
  const { items } = await chrome.storage.local.get('items');
  const batchSize = 100;
  
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await processBatch(batch);
    await new Promise(r => setTimeout(r, 0)); // Yield to main thread
  }
}

See Also

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.