Architecture
Components
┌─────────────────────────────────────────────┐│ Browser ││ ┌──────────┐ ┌──────────┐ ┌───────────┐ ││ │ injected │◄►│ content │◄►│background │ ││ │ (main │ │ (isolated│ │ (service │ ││ │ world) │ │ world) │ │ worker) │ ││ └──────────┘ └──────────┘ └─────┬─────┘ │└─────────────────────────────────────┼───────┘ │ ┌─────────────┼──────────────┐ │ │ │ ┌───────▼───────┐ ┌──▼───────────┐ │ │ chrome.storage│ │ Cloudflare │ │ │ .sync │ │ Worker + D1 │ │ │ (default) │ │ (optional) │ │ └───────────────┘ └──────────────┘ │ │ │ └────────────────────────────┘Sync Backends
StashBridge supports two sync backends:
Browser Sync (Default)
Uses chrome.storage.sync — the browser’s built-in synced storage. Data is compressed with LZ-String and optionally encrypted with AES-256-GCM before being stored. Syncs automatically via Chrome Sync, Firefox Sync, or Brave Sync.
Constraints: 100KB total, 8KB per item, 512 items max, same browser brand only.
Server Relay (Optional)
Uses a Cloudflare Worker + D1 database. No storage limits (practical). Works across different browser brands. Requires deployment.
The Two-Script Bridge
Browser extensions run content scripts in an isolated world — they can’t access the page’s localStorage. StashBridge solves this with two scripts:
-
injected.ts— Runs in the page’s main world. Monkey-patchesStorage.prototype.setItemandremoveItemto intercept changes. Communicates viaCustomEventondocument. -
content.ts— Runs in the isolated world. Listens forCustomEvents from the injected script and forwards them to the background viachrome.runtime.sendMessage(). Also relays remote changes back.
This is necessary because:
- The injected script can access
localStoragebut notchrome.runtime - The content script can access
chrome.runtimebut not the page’slocalStorage CustomEventondocumentbridges the gap
Sync Engine
The background service worker manages all sync logic:
- Push: Batches local changes, debounces (500ms), pushes via
POST /sync - Pull: Every 60 seconds via
chrome.alarms, fetches changes viaGET /pull?since= - Persistence: All state (pending changes, last sync time) is in
chrome.storage.local, surviving service worker termination - Whitelist filtering: Only processes changes for explicitly whitelisted
{origin, key}pairs
Conflict Resolution
Last-write-wins by updated_at timestamp per key:
ON CONFLICT(origin, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_atWHERE excluded.updated_at > sync_entries.updated_atIf Browser A writes at t=100 and Browser B writes at t=101, Browser B’s value wins regardless of push order.
Re-entry Guard
When applying remote changes, the injected script must not re-capture them as local changes:
let suppressCapture = false;
// When applying remote change:suppressCapture = true;originalSetItem.call(localStorage, key, value);suppressCapture = false;
// When capturing local change:if (!suppressCapture) { // dispatch event...}