Chrome Extension Updates — How to Handle Version Upgrades and Data Migration
20 min readChrome Extension Updates — How to Handle Version Upgrades and Data Migration
When you publish a Chrome extension, your work doesn’t end at launch. Users expect seamless updates, and as your extension evolves, you’ll need to handle version upgrades gracefully while preserving user data. This guide covers the essential patterns for managing extension updates, implementing data migrations, and handling breaking changes without losing your users’ trust or their valuable data.
Understanding chrome.runtime.onInstalled
The chrome.runtime.onInstalled event is the foundation of any update management strategy. This event fires when your extension is first installed, updated to a new version, or when Chrome itself is updated. Understanding when and how this event fires is critical for proper initialization and migration handling.
Chrome Extension Update Strategies
Introduction
- Chrome extensions require careful update management to ensure users receive new features, bug fixes, and security patches
- Poor update strategies can lead to user data loss, broken functionality, and negative reviews
- This guide covers auto-updates, versioning, migration, testing, and deployment strategies
Auto-Update Mechanism
How Chrome Auto-Updates Work
- Chrome checks for extension updates every few hours (typically 5-6 hours)
- Update check is triggered by the
update_urlin manifest.json - For Web Store extensions:
https://clients2.google.com/service/update2/crx - Chrome downloads the new CRX file, verifies the signature, and installs automatically
- Users are notified via chrome://extensions “Update” button or automatic notification
- Extensions loaded unpacked (
--load-extension) do NOT auto-update
Update Check Frequency
chrome.runtime.onInstalled.addListener((details) => {
switch (details.reason) {
case "install":
handleFirstInstall();
break;
case "update":
handleVersionUpgrade(details.previousVersion);
break;
case "chrome_update":
handleChromeUpdate();
break;
}
});
The details object provides crucial information: reason tells you why the event fired, previousVersion reveals what version users were on before the update (only available when reason is “update”), and id identifies your extension. This distinction between install, update, and Chrome update scenarios allows you to execute different logic for each case, ensuring first-time users get a proper welcome while existing users have their data migrated seamlessly.
Version Checking and Comparison
Before running any migration, you need reliable version comparison logic. Version strings like “1.2.3” require careful parsing because simple string comparison fails with numbers like “1.10.0” being considered less than “1.2.0” in lexicographic ordering.
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 < p2) return -1;
if (p1 > p2) return 1;
}
return 0;
}
function needsMigration(currentVersion, targetVersion) {
return compareVersions(currentVersion, targetVersion) < 0;
}
function isMajorVersionBump(oldVersion, newVersion) {
const oldMajor = parseInt(oldVersion.split('.')[0], 10);
const newMajor = parseInt(newVersion.split('.')[0], 10);
return newMajor > oldMajor;
}
Version checking serves multiple purposes: determining which migrations to run, deciding whether to show a “What’s New” dialog, and identifying breaking changes that require special handling. Store your current schema version in chrome.storage.local so you can track what migrations have already been applied.
Data Migration Scripts
Data migration is perhaps the most critical aspect of extension updates. When you change your storage schema, you must transform existing user data to match the new structure. Without proper migrations, users upgrading from older versions will lose their settings or encounter errors. // Manifest V3 - automatic, no code needed // Chrome handles update checks internally based on: // - Extension ID // - Update URL in manifest // - Current version
## Version Numbering Best Practices
### Semantic Versioning (SemVer) for Extensions
- Format: `MAJOR.MINOR.PATCH` (e.g., 2.1.0)
- MAJOR: Breaking changes, removed features, significant architecture changes
- MINOR: New features, backward-compatible functionality
- PATCH: Bug fixes, performance improvements, security patches
- Pre-release versions: `1.0.0-beta.1`, `1.0.0-rc.2`
### Version Rules in manifest.json
```json
{
"manifest_version": 3,
"version": "2.1.0",
"version_name": "2.1.0 Beta"
}
versionis required and must be valid (MAJOR.MINOR.PATCH)version_nameis optional for user-facing version display
Best Practices
- Always increment version for each published update
- Don’t skip versions (1.0.0 → 1.0.2 is confusing)
- Use
version_namefor beta/RC releases - Document version history in CHANGELOG.md
Breaking Changes Handling
Identifying Breaking Changes
- Removed APIs or parameters
- Changed data structures
- Incompatible storage schemas
- Modified permissions requirements
- Different content script injection behavior
Strategies for Safe Breaking Changes
- Deprecation warnings before removal
- Feature flags for gradual rollout
- Backward compatibility layers
- Migration utilities for user data
Example: Feature Flag Migration
// background.js
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
const previousVersion = details.previousVersion;
// Migrate from old feature to new feature
if (semver.lt(previousVersion, '2.0.0')) {
await migrateToV2();
}
// Handle specific version jumps
if (semver.lt(previousVersion, '2.1.0')) {
await migrateToV2_1();
}
}
});
async function migrateToV2() {
const oldData = await chrome.storage.local.get('oldSetting');
if (oldData.oldSetting) {
await chrome.storage.local.set({
newSetting: transformSetting(oldData.oldSetting)
});
await chrome.storage.local.remove('oldSetting');
}
}
Data Migration Between Versions
Storage Migration Pattern
// migrations.js - Centralized migration manager
const MIGRATIONS = {
'1.0.0': migrateFrom1_0_0,
'1.1.0': migrateFrom1_1_0,
'2.0.0': migrateFrom2_0_0
};
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
const previousVersion = details.previousVersion;
const currentVersion = chrome.runtime.getManifest().version;
for (const [version, migrateFn] of Object.entries(MIGRATIONS)) {
if (semver.lt(previousVersion, version)) {
console.log(`Running migration for version ${version}`);
await migrateFn();
}
}
}
});
async function migrateFrom1_1_0() {
// Example: rename storage keys
const data = await chrome.storage.local.get(['oldKey1', 'oldKey2']);
if (data.oldKey1) {
await chrome.storage.local.set({
newKey1: data.oldKey1,
newKey2: data.oldKey2 || 'default'
});
await chrome.storage.local.remove(['oldKey1', 'oldKey2']);
}
}
Migration Checklist
- Document all storage keys and their purposes
- Create migration functions for each version
- Test migration path from oldest supported version
- Handle migration failures gracefully
- Provide fallback for corrupted data
- Log migration status for debugging
chrome.runtime.onInstalled for Update Detection
Basic Usage
const MIGRATIONS = [
{ from: '1.0.0', to: '1.1.0', migrate: migrateV1toV11 },
{ from: '1.1.0', to: '1.2.0', migrate: migrateV11toV12 },
{ from: '1.2.0', to: '2.0.0', migrate: migrateV12toV20 },
];
async function runMigrations(previousVersion) {
const currentVersion = chrome.runtime.getManifest().version;
for (const migration of MIGRATIONS) {
if (compareVersions(previousVersion, migration.from) >= 0 &&
compareVersions(previousVersion, migration.to) < 0) {
console.log(`Running migration: ${migration.from} -> ${migration.to}`);
await migration.migrate();
}
}
await chrome.storage.local.set({ schemaVersion: getSchemaVersion(currentVersion) });
}
async function migrateV11toV12() {
const oldData = await chrome.storage.local.get(['userSettings', 'cachedData']);
const newData = {
preferences: {
theme: oldData.userSettings?.theme || 'light',
notifications: oldData.userSettings?.notify ?? true,
},
cache: {
data: oldData.cachedData,
timestamp: Date.now(),
}
};
await chrome.storage.local.set(newData);
await chrome.storage.local.remove(['userSettings', 'cachedData']);
}
Effective migration scripts follow several key principles: always read from the old structure and write to the new one, never assume old data exists (provide defaults), remove old keys after successful migration, and log progress for debugging. Consider wrapping migrations in try-catch blocks to handle unexpected data formats gracefully.
Handling Breaking Changes
Breaking changes require extra care because they can disrupt user workflows or cause data loss. When you introduce breaking changes, communicate clearly through release notes and consider providing transitional compatibility layers.
async function handleBreakingChange(previousVersion, currentVersion) {
if (isMajorVersionBump(previousVersion, currentVersion)) {
// Show breaking changes notice
await chrome.storage.local.set({
showBreakingChangesNotice: true,
breakingChangesVersion: currentVersion
});
// Offer data backup before major changes
await backupUserData(previousVersion);
}
}
Breaking changes often involve API deprecations, storage schema redesigns, or feature removals. For API changes, consider maintaining backward compatibility through wrapper functions that handle both old and new patterns. For storage changes, ensure migrations preserve user intent even when the underlying structure changes dramatically.
Rollback Strategies
Sometimes an update causes problems that aren’t immediately apparent. Having a rollback strategy protects both your users and your reputation. Chrome doesn’t support true rollbacks, but you can implement logical rollbacks through version detection and data recovery.
async function safeMigration(migrationFn, fallbackFn) {
try {
// Create backup before migration
const backup = await chrome.storage.local.get();
await chrome.storage.local.set({
preMigrationBackup: JSON.stringify(backup),
backupVersion: chrome.runtime.getManifest().version
});
// Run migration
await migrationFn();
} catch (error) {
console.error('Migration failed:', error);
// Attempt rollback
if (fallbackFn) {
await fallbackFn();
} else {
// Restore from backup
const backup = await chrome.storage.local.get('preMigrationBackup');
if (backup.preMigrationBackup) {
const restored = JSON.parse(backup.preMigrationBackup);
await chrome.storage.local.set(restored);
}
}
// Notify user of issues
await chrome.notifications.create({
type: 'basic',
iconUrl: 'images/icon128.png',
title: 'Update Issue',
message: 'We encountered an issue during update. Your data has been preserved.'
});
}
}
Rollback strategies should include automatic data backup before migrations, version-specific fallback behaviors, clear user communication when issues occur, and logging for post-mortem analysis. Consider implementing a “safe mode” that disables new features while preserving core functionality.
Testing Your Update Flow
Thorough testing is essential because update scenarios are difficult to reproduce and users run versions across a wide spectrum. Create a systematic testing approach that covers fresh installs, various upgrade paths, and edge cases.
Test your update handlers by loading your extension, making code changes, clicking “Update” in chrome://extensions, and examining console output. Verify storage contains the correct schema version after migration and test with pre-existing data from older versions. Use Chrome’s profile management to create test profiles with different extension versions installed.
Summary
Managing Chrome extension updates requires careful planning and robust implementation. Use chrome.runtime.onInstalled to detect installation, updates, and Chrome changes. Implement version comparison utilities to determine which migrations to run. Create migration scripts that transform old data to new schemas without data loss. Handle breaking changes with clear communication and optional compatibility layers. Maintain rollback capabilities through backup and recovery mechanisms.
With proper update handling, your users can upgrade with confidence, knowing their data will be preserved and their experience will remain uninterrupted regardless of which version they’re coming from.
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one. chrome.runtime.onInstalled.addListener((details) => { switch (details.reason) { case ‘install’: console.log(‘Extension installed for the first time’); initializeDefaultSettings(); showWelcomePage(); break;
case 'update':
console.log(`Updated from ${details.previousVersion}`);
handleUpdate(details.previousVersion);
break;
case 'chrome_update':
console.log('Chrome browser updated');
break; } });
async function handleUpdate(previousVersion) { // Show changelog for significant updates if (semver.major(previousVersion) !== semver.major(chrome.runtime.getManifest().version)) { showMajorUpdateNotification(); } }
### Detecting Update Type
```javascript
function getUpdateType(previousVersion, currentVersion) {
const prev = semver.parse(previousVersion);
const curr = semver.parse(currentVersion);
if (prev.major !== curr.major) return 'major';
if (prev.minor !== curr.minor) return 'minor';
return 'patch';
}
Update Notification Patterns
In-App Notifications
// Show update notification in popup or options page
async function showUpdateNotification(previousVersion) {
const changelog = await fetchChangelog(previousVersion);
const notification = {
type: 'basic',
iconUrl: 'images/icon48.png',
title: 'Extension Updated!',
message: `Version ${chrome.runtime.getManifest().version} is now available.`,
priority: 1,
buttons: [
{ title: 'View Changes' },
{ title: 'Dismiss' }
]
};
chrome.notifications.create('update-notification', notification);
}
Changelog Display
// Display changelog to user after update
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'update') {
// Show changelog in a new tab
chrome.tabs.create({
url: 'changelog.html',
active: false
});
}
});
// changelog.html - fetch and display recent changes
(async () => {
const response = await fetch('CHANGELOG.md');
const changelog = await response.text();
document.getElementById('changelog').textContent = changelog;
})();
Staged Rollouts in Chrome Web Store
Understanding Staged Rollouts
- Chrome Web Store supports gradual rollout percentages
- Start with 1-5% of users, then increase after monitoring
- Allows catching critical bugs before full release
- Available in developer dashboard under “Distribution”
Staged Rollout Strategy
- Upload new version as draft
- Test with trusted testers
- Publish to 1-5% rollout
- Monitor crash reports and reviews
- Increase rollout percentage incrementally
- Reach 100% after confidence is high
Monitoring During Rollout
- Check Chrome Web Store developer dashboard
- Monitor chrome://extensions errors
- Review user feedback and ratings
- Track storage error rates in telemetry
Rollback Strategies
Preventing Bad Updates
- Always test locally before publishing
- Use staged rollouts to catch issues early
- Keep previous version CRX for emergency rollback
- Maintain a “known good” version reference
Emergency Rollback Process
- Go to Chrome Web Store developer dashboard
- Select the extension
- Upload previous version CRX
- Set as active version
- Push to 100% rollout immediately
- Monitor for stabilization
Version Preservation
# Keep old CRX files for emergency rollback
/extensions/
├── myextension-1.0.0.crx
├── myextension-1.0.1.crx
└── myextension-1.1.0.crx
Forced Updates for Security Fixes
Implementing Forced Updates
// Check minimum required version on startup
const MINIMUM_VERSION = '2.1.0';
async function checkForcedUpdate() {
const currentVersion = chrome.runtime.getManifest().version;
if (semver.lt(currentVersion, MINIMUM_VERSION)) {
// Show urgent update notification
chrome.notifications.create({
type: 'basic',
iconUrl: 'images/warning.png',
title: 'Security Update Required',
message: 'A critical security update is required. Please update now.',
priority: 2,
buttons: [{ title: 'Update Now' }]
});
// Disable extension functionality until updated
await chrome.storage.local.set({ extensionEnabled: false });
}
}
Security Update Checklist
- Communicate urgency clearly
- Provide one-click update path
- Consider auto-update delay (48-72 hours)
- Have rollback plan ready
- Document the security vulnerability
Self-Hosted Extension Updates
Update Manifest XML Format
For self-hosted extensions, Chrome checks an XML manifest for updates:
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0' server='prod'>
<app appid='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'>
<updatecheck
codebase='https://example.com/extensions/myextension.crx'
version='2.1.0'
hash='sha256=abc123...'/>
</app>
</gupdate>
Hosting the Update Manifest
// Example: Dynamic XML generation (server-side)
app.get('/update.xml', (req, res) => {
const updateXml = `<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0' server='prod'>
<app appid='${EXTENSION_ID}'>
<updatecheck
codebase='https://example.com/updates/extension-${latestVersion}.crx'
version='${latestVersion}'
hash='sha256=${fileHash}'/>
</app>
</gupdate>`;
res.type('application/xml').send(updateXml);
});
Manifest.json Update URL
{
"manifest_version": 3,
"name": "My Extension",
"version": "2.1.0",
"update_url": "https://example.com/update.xml"
}
Reference: https://developer.chrome.com/docs/extensions/develop/distribute/host-on-linux
Testing Updates Locally
Local Testing Methods
- Load unpacked extension:
chrome://extensions→ “Load unpacked” - Use “Update” button to reload after changes
- Test
chrome.runtime.onInstalledby reinstalling - Test data migration with development storage
Simulating Updates
// Test migration logic without actual update
async function testMigration() {
// Set up "old" storage state
await chrome.storage.local.set({
oldSetting: 'legacy-value',
oldData: { items: [1, 2, 3] }
});
// Simulate update event
const mockDetails = {
reason: 'update',
previousVersion: '1.0.0'
};
// Run migration
await handleUpdate(mockDetails);
// Verify migration results
const result = await chrome.storage.local.get(['newSetting', 'newData']);
console.log('Migration result:', result);
}
Testing Checklist
- Test fresh install flow
- Test update from oldest supported version
- Test update from each major version
- Test data migration with real data
- Test forced update scenario
- Test rollback behavior
Update Architecture Best Practices
Summary Checklist
- Use semantic versioning consistently
- Implement robust data migration system
- Use chrome.runtime.onInstalled for update handling
- Display changelog after major updates
- Use staged rollouts in Chrome Web Store
- Keep previous versions for emergency rollback
- Implement forced updates for security-critical patches
- Test all migration paths thoroughly
- Monitor update success/failure rates
- Communicate changes clearly to users
Recommended Tools
semvernpm package for version comparison- Chrome Storage for migration state
- Chrome Web Store publishing API for automation
- CRX viewer for inspecting published extensions