v148 Β· PWA Β· Service Workers
Service Worker Bridge
The trickiest part of PWA origin migration: transferred install identity is declarative, but service worker state remains origin-scoped. Probe the current browser APIs, vary bridge options, and see which cache and messaging steps need app code.
Chrome 148 adds the manifest
migrate_to / migrate_from handshake for same-site installed-app moves. The manifest path handles the installed app identity after validation; caches, IndexedDB, sync queues, and push endpoints still need explicit app-level migration code or replacement UX.
Three-origin architecture
Old Origin
sourcehttps://app.example.com
- Cache API entries
- Background Sync queues
- Push subscription
- IndexedDB stores
- Fetch intercept
β
New Origin
destinationhttps://www.example.com
- Receives transferred cache
- Re-registers sync tags
- New push subscription
- Reads migrated IDB data
- Full fetch control
Runtime bridge probe
This uses APIs available on this page where possible. It cannot trigger the browser's declarative manifest migration, so unsupported or missing APIs are surfaced as fallback states.
4 responses
Transfer Cache API entries
Export IndexedDB records
Attempt push resubscribe plan
Ready. Choose bridge options, then run the probe.
State inventory: what needs migrating
Click an item to see the transfer mechanism and code snippet.
Cache API
Cached responses
manual
Background Sync
Pending sync tags
manual
Push Subscription
VAPID subscription
user action
Notification Permission
granted / denied
user action
IndexedDB
App data stores
manual
PWA Install
Home screen shortcut
auto (148)
Simulate the migration
1
Old origin SW intercepts navigation
Waitingβ¦
2
Bridge: transfer cache to new origin via postMessage
Waitingβ¦
3
New origin SW activates and receives handoff
Waitingβ¦
Bridge SW code patterns
Old origin SW β fetch interception + redirect
// old-origin/sw.js β intercept navigation, redirect to new origin
const NEW_ORIGIN = 'https://www.example.com';
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Navigation requests: redirect to new origin
if (event.request.mode === 'navigate') {
const newUrl = NEW_ORIGIN + url.pathname + url.search + url.hash;
event.respondWith(Response.redirect(newUrl, 301));
return;
}
// Subresources: serve from cache, then network fallback
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
});
New origin SW β receive cache handoff
// new-origin/sw.js β listen for cache transfer from old origin
self.addEventListener('message', async (event) => {
if (event.data?.type !== 'CACHE_TRANSFER') return;
const { cacheName, entries } = event.data;
const cache = await caches.open(cacheName);
for (const { url, body, headers } of entries) {
const response = new Response(body, { headers });
await cache.put(url, response);
}
event.ports[0]?.postMessage({ ok: true, count: entries.length });
});
Migration page β coordinate the handoff
// migration-page.js β runs on old origin during the transition window
async function transferCachesToNewOrigin() {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const keys = await cache.keys();
const entries = [];
for (const request of keys) {
const response = await cache.match(request);
const body = await response.arrayBuffer();
entries.push({
url: request.url,
body,
headers: Object.fromEntries(response.headers.entries()),
});
}
// Post to new origin iframe/worker
newOriginFrame.contentWindow.postMessage(
{ type: 'CACHE_TRANSFER', cacheName: name, entries },
'https://www.example.com'
);
}
}