DEV Community

Cover image for Five Tools, Zero Interference - CaptureTools Isolation Architecture
Dhardingsea Developer
Dhardingsea Developer

Posted on

Five Tools, Zero Interference - CaptureTools Isolation Architecture

Building five separate tools inside a single Chrome extension sounds straightforward until you realize it's a recipe for disaster. Without deliberate isolation, one tool's storage leaks into another's, message handlers step on each other, and a single runtime collision brings everything down.

This post walks through four structural mechanisms that keep CaptureTools running smoothly—no tool interference, no data leaks, no architectural finger-pointing.

The Problem

A single Chrome extension is one runtime, one message bus, one storage layer.

When five tools run in that space, they're sharing everything:

  • chrome.storage.sync (the same key namespace)
  • Message event listeners (all broadcasting to the same channel)
  • Content script registry (one global scope)

Without boundaries, tool A's data ends up in tool B's key namespace. Tool C's message handler fires on tool D's request. Unregistering one tool leaves the others hung. It's not if something breaks—it's which failure mode hits first.

The Solution: Four Isolation Layers

1. Storage Isolation via Prefix-Locked Adapters

Every tool gets a dedicated storage key prefix:

  • ct:sealshot:state
  • ct:backtrack:history
  • ct:lantern:settings

A storage adapter wraps calls to chrome.storage.sync and enforces the prefix on every read and write. If Sealshot somehow tries to touch ct:backtrack:state, the adapter rejects it.

This isn't about trust. It's about making mistakes structurally impossible at the API boundary.

// Example adapter interface
const storageAdapter = {
  get: async (keys) => {
    // Prefixes all keys with tool ID before calling chrome.storage.sync
    const prefixed = keys.map(k => `ct:${toolId}:${k}`);
    return chrome.storage.sync.get(prefixed);
  },
  set: async (items) => {
    // Enforces prefix; rejects any attempt to cross the boundary
    const prefixed = Object.fromEntries(
      Object.entries(items).map(([k, v]) => [`ct:${toolId}:${k}`, v])
    );
    return chrome.storage.sync.set(prefixed);
  }
};
Enter fullscreen mode Exit fullscreen mode

The pattern is lightweight. Tools use the same storage API they'd use anywhere else; the isolation just sits underneath.

2. Message Addressing by Tool ID

Chrome's message API broadcasts listeners across the extension. Any script can send a message; any listener can receive it.

CaptureTools routes by tool ID. Every message carries a tool field:

// Sending
chrome.runtime.sendMessage({
  tool: 'sealshot',
  action: 'capture',
  data: { ... }
});

// Receiving
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  // Only process messages meant for this tool
  if (msg.tool !== 'backtrack') return;
  // Handle message...
});
Enter fullscreen mode Exit fullscreen mode

This prevents broadcast storms, message handler collisions, and one tool accidentally triggering another's side effects. It's a simple protocol, but it moves responsibility to the caller—and that's exactly where it belongs.

3. Database Namespacing

CaptureTools uses IndexedDB for larger datasets. Each tool gets a separate object store namespace:

// Sealshot's stores
const sealshot_cache = db.createObjectStore('sealshot_cache');
const sealshot_metadata = db.createObjectStore('sealshot_metadata');

// Backtrack's stores
const backtrack_history = db.createObjectStore('backtrack_history');
const backtrack_snapshots = db.createObjectStore('backtrack_snapshots');
Enter fullscreen mode Exit fullscreen mode

A database adapter wraps all IndexedDB calls and routes them to the correct namespace. One tool's query never touches another tool's data.

This layer is especially critical because IndexedDB doesn't have built-in ACLs. Namespacing is the only control plane.

4. Content Script Lifecycle Management

When a tool is disabled, its content scripts must unregister completely.

CaptureTools tracks which content script belongs to which tool and performs full cleanup:

  • Deregister from the message bus
  • Clear DOM state
  • Remove event listeners
  • Unsubscribe from runtime events
// Cleanup on disable
const unregisterTool = (toolId) => {
  // Remove all message listeners for this tool
  chrome.runtime.onMessage.removeListener(
    handlers.get(`${toolId}_messageHandler`)
  );

  // Unregister content scripts from tabs
  chrome.tabs.query({}, (tabs) => {
    tabs.forEach(tab => {
      chrome.tabs.sendMessage(tab.id, {
        tool: toolId,
        action: 'unregister'
      }).catch(() => {}); // Tab may not have script
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

This prevents ghost listeners firing on messages meant for other tools and orphaned state lingering after uninstall. It's the most underrated failure mode: a tool that's gone in the UI but still listening in the background.

Design Tradeoffs

This architecture trades runtime overhead for complete failure isolation:

  • Runtime cost: Adapter calls, namespace checks, lifecycle management
  • Benefit: A tool crash doesn't cascade. Storage errors in one tool don't corrupt others.

It's not zero-cost, but it's predictable, testable, and scales with the number of tools.

Questions?

This is a working implementation. Things might shift as tooling evolves. If you have questions about isolation strategy, multi-tool extension design, or how this compares to other approaches, I'm curious to hear your thoughts.

For you: If you're building a multi-tool extension or wondering how to keep separate systems in one runtime, does this pattern help? If you're using CaptureTools and curious about stability, now you know.

Also—if you're building something similar, what's your isolation strategy? I'd love to know what works for you and what doesn't.


Read the full post with examples and FAQ: https://dhseadev.online/2026/08/27/capturetools-isolation-architecture/

Top comments (0)