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

source
https://app.example.com
  • πŸ“¦ Cache API entries
  • πŸ”„ Background Sync queues
  • πŸ”” Push subscription
  • πŸ—ƒ IndexedDB stores
  • 🌐 Fetch intercept
β†’

New Origin

destination
https://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'
    );
  }
}

references