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:statect:backtrack:historyct: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);
}
};
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...
});
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');
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
});
});
};
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)