Engineering15 min read7 sections

Manifest V3 Architecture Deep Dive: Building Production Browser Extensions in 2026

A comprehensive technical guide to Chrome Manifest V3 architecture — service workers, content scripts, message passing, storage, and the real-world challenges of building production extensions for AI platforms.

Abhay Donde

Founder & Lead Engineer at Toffee

Published July 1, 2026Updated July 28, 2026

Introduction: Why Manifest V3 Changed Everything

In January 2023, Google began enforcing Manifest V3 (MV3) as the required format for all new Chrome extensions, deprecating the decade-old Manifest V2. This was not a minor version bump — it fundamentally restructured how browser extensions operate, affecting security, performance, and developer experience.

For developers building AI-related extensions, MV3 introduced both challenges and opportunities. The migration from persistent background pages to ephemeral service workers required rethinking state management. Stricter Content Security Policies improved security but complicated dynamic code loading. New declarative APIs replaced powerful but abuse-prone imperative ones.

This article provides a production-focused guide to MV3 architecture, drawn from practical experience building Toffee — a browser extension that captures, compresses, and transfers AI conversation context across platforms.

MV3 Architecture Overview

A Manifest V3 extension consists of several isolated execution contexts that communicate through message passing:

  • Service Worker (background.js): Replaces MV2's background page. Handles events, manages state, and orchestrates extension logic. Crucially, it is ephemeral — Chrome terminates it after 30 seconds of inactivity and restarts it when needed.
  • Content Scripts: JavaScript injected into web pages. Has DOM access but runs in an isolated world with no access to the page's JS context. In MV3, content scripts must be statically declared in the manifest or injected via the chrome.scripting API.
  • Popup / Side Panel: The extension's UI, rendered in a separate window. It has access to Chrome extension APIs but no direct DOM access to web pages.
  • Options Page: A dedicated page for extension settings.

The communication flow for a typical AI context capture looks like this:

User clicks "Capture" in Popup
  → Popup sends message to Service Worker
    → Service Worker injects content script via chrome.scripting
      → Content script reads AI platform DOM
        → Content script sends extracted data to Service Worker
          → Service Worker compresses and stores in IndexedDB
            → Service Worker sends confirmation to Popup
              → Popup updates UI

Each arrow represents a message passing boundary. Understanding these boundaries is essential for debugging MV3 extensions.

The Service Worker Lifecycle: Managing Ephemeral State

The most impactful change in MV3 is the replacement of persistent background pages with ephemeral service workers. In MV2, the background page ran continuously, maintaining state in memory. In MV3, the service worker is terminated after 30 seconds of inactivity.

This has profound implications for state management:

// ❌ This will NOT work in MV3 — variable is lost on restart
let capturedData: ConversationData[] = [];

chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === 'CAPTURE') {
    capturedData.push(msg.data); // Lost when SW terminates
  }
});

// ✅ Use persistent storage instead
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === 'CAPTURE') {
    // IndexedDB persists across service worker restarts
    db.captures.put(msg.data);
  }
});

Any state that must survive a service worker restart must be persisted to IndexedDB, Chrome storage (chrome.storage.local), or session storage (chrome.storage.session).

Keep-Alive Patterns

For long-running operations like AI compression (which can take 5-15 seconds), the service worker might terminate mid-operation. There are several patterns to prevent this:

// Pattern 1: Use chrome.runtime.onMessage's sendResponse
// The service worker stays alive while awaiting sendResponse
chrome.runtime.onMessage.addListener(
  (message, sender, sendResponse) => {
    if (message.type === 'COMPRESS') {
      compressConversation(message.data)
        .then((bundle) => sendResponse({ bundle }))
        .catch((err) => sendResponse({ error: err.message }));
      return true; // Keep alive until sendResponse is called
    }
  }
);

// Pattern 2: Self-messaging ping to reset the 30s timer
async function longRunningTask() {
  const keepAlive = setInterval(() => {
    chrome.runtime.sendMessage({ type: 'KEEPALIVE' });
  }, 20000);
  
  try {
    const result = await expensiveOperation();
    return result;
  } finally {
    clearInterval(keepAlive);
  }
}

Pattern 1 is preferred because it uses the platform's own lifecycle management. Pattern 2 is a workaround for operations that do not originate from a message handler.

Content Script Injection: Static vs. Programmatic

MV3 supports two methods of injecting content scripts, each with distinct trade-offs:

Static Declaration (manifest.json)

{
  "content_scripts": [{
    "matches": ["https://chatgpt.com/*"],
    "js": ["content/chatgpt.js"],
    "run_at": "document_idle"
  }]
}

Static scripts are automatically injected when the user navigates to a matching URL. They are simple to configure but run on every page load, consuming resources even when the user does not intend to capture context.

Programmatic Injection (chrome.scripting)

// Inject only when user requests a capture
async function injectContentScript(tabId: number, platform: string) {
  const scriptFile = `content/${platform}.js`;
  
  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      files: [scriptFile],
      world: 'ISOLATED', // Default — safe sandbox
    });
  } catch (error) {
    // Fallback: some sites reject ISOLATED world injection
    await chrome.scripting.executeScript({
      target: { tabId },
      files: [scriptFile],
      world: 'MAIN', // Runs in page's JS context
    });
  }
}

Programmatic injection is more efficient because scripts are loaded only when needed. However, it requires the scripting permission and host_permissions for the target domains.

The fallback from ISOLATED to MAIN world is important for platforms that restrict extension execution. Some enterprise-deployed AI tools block content scripts in the isolated world via their Content Security Policy. Injecting into the MAIN world bypasses this restriction, though it requires extra care to avoid variable name conflicts with the page's own scripts.

Storage Strategies: IndexedDB, Chrome Storage, and Dexie

MV3 extensions have several storage options, each suited to different data types:

StorageCapacityAccess FromBest For
chrome.storage.local10 MB (unlimited with permission)Service Worker, Popup, Content ScriptsSettings, preferences, small state
chrome.storage.session10 MBService Worker, PopupEphemeral session state (cleared on browser close)
IndexedDBUnlimited (browser-dependent)Service Worker, PopupLarge data: conversation bundles, compressed context
chrome.storage.sync100 KB totalAll contextsCross-device settings sync

For AI context bundles that can be 10–100 KB each, IndexedDB is the only practical choice. Using Dexie — a typed wrapper around IndexedDB — provides a developer experience comparable to an ORM:

import Dexie, { Table } from 'dexie';

interface StoredBundle {
  id: string;
  platform: string;
  title: string;
  compressedContext: string;
  metrics: BundleMetrics;
  createdAt: Date;
  synced: boolean;
}

class ToffeeDB extends Dexie {
  bundles!: Table;
  
  constructor() {
    super('ToffeeDB');
    this.version(1).stores({
      bundles: 'id, platform, createdAt, synced',
    });
  }
}

export const db = new ToffeeDB();

Dexie handles schema versioning, indexing, and querying with a clean Promise-based API. This is dramatically more productive than the raw IndexedDB API, which uses an event-driven pattern that is difficult to compose with async/await code.

Content Security Policy and Security Model

MV3 enforces a strict Content Security Policy for all extension contexts:

  • No inline scripts (script-src 'self')
  • No eval() or new Function()
  • No remote code execution
  • All scripts must be bundled as static files within the extension package

This is a significant security improvement over MV2, which allowed extensions to fetch and execute arbitrary code from remote servers. For users, this means an MV3 extension's behavior is fully auditable from its packaged source code — it cannot download and run new code after installation.

For developers, this means build tooling must produce self-contained bundles. Dynamic imports are allowed within the extension's own files but cannot reference external URLs. Vite and Webpack are commonly used to produce these bundles, with special plugins like @crxjs/vite-plugin that handle the unique requirements of extension bundling.

The security model also affects how content scripts interact with AI platform pages. Content scripts in the ISOLATED world cannot access the page's JavaScript runtime, which means they cannot call functions defined by the page or access React component state. They can only read and modify the DOM.

Testing and Debugging MV3 Extensions

Debugging MV3 extensions requires understanding where each component runs:

  • Service Worker: Inspectable at chrome://extensions → "Inspect views: service worker." The DevTools opens in a separate window.
  • Content Scripts: Visible in the page's DevTools under Sources → Content Scripts. Console output appears in the page's console.
  • Popup: Right-click the extension icon → "Inspect Popup." DevTools opens attached to the popup window.

A common pitfall: service worker logs disappear when the worker is terminated. To preserve logs across restarts, either persist them to storage or keep the service worker's DevTools window open (which prevents termination — useful during development but not in production).

For automated testing, tools like Puppeteer and Playwright support loading unpacked extensions. This enables end-to-end testing of the full capture-compress-store pipeline without manual interaction. Combining this with CI/CD ensures that platform adapter updates do not break existing functionality.

Frequently Asked Questions

Q1

What is the difference between Manifest V2 and Manifest V3?

Manifest V3 replaces persistent background pages with ephemeral service workers, enforces stricter Content Security Policies that prevent remote code execution, introduces the declarativeNetRequest API to replace webRequest blocking, and requires static content script declaration. These changes improve security and performance at the cost of development complexity.

Q2

Why does the service worker terminate after 30 seconds?

Chrome terminates idle service workers to conserve system resources. Extensions that run persistent background processes can drain battery and memory. The 30-second timeout ensures extensions only consume resources when actively processing events.

Q3

Can Manifest V3 extensions work in Firefox?

Firefox supports MV3 with some differences. Firefox allows both MV2 and MV3, and its MV3 implementation retains some MV2 features like Event Pages. Extensions targeting both Chrome and Firefox need to account for these differences in their build configuration.

Manifest V3Browser ExtensionsService WorkersChrome ExtensionsWeb Platform

Continue Reading