Core APIs & Cross-Browser Data Management
Master MV3 storage, messaging, declarativeNetRequest, tabs, and scripting APIs — with cross-browser compatibility patterns for Chrome, Firefox, and Safari extensions.
Master MV3 storage, messaging, declarativeNetRequest, tabs, and scripting APIs — with cross-browser compatibility patterns for Chrome, Firefox, and Safari extensions.
Manifest V3 ships a compact but opinionated API surface: storage that outlives the background service worker, declarative network rules that replace blocking listeners, a messaging bus that wires isolated contexts together, tabs/windows APIs that require explicit host permissions, and a scripting API that injects code on demand. Getting each subsystem right in isolation is necessary but not sufficient — the hard problems emerge at the seams, when a storage write must trigger a network rule update, or when a content script needs a response from a service worker that may have been evicted mid-flight. The Chrome Storage API & Sync guide is the best starting point for most extensions because durable state is the backbone everything else depends on.
Declaring the wrong permission scope is the most common cause of silent failures. The snippet below shows the full declaration surface for every subsystem covered here, with comments explaining which grant enables what.
1{
2 "manifest_version": 3,
3 "name": "My Extension",
4 "version": "1.0",
5 "permissions": [
6 "storage", // chrome.storage.local/sync/session
7 "declarativeNetRequest", // static DNR rules (no host perms needed)
8 "declarativeNetRequestFeedback", // optional: lets you read matched rules
9 "tabs", // read tab URLs/titles; query active tab
10 "scripting" // chrome.scripting.executeScript/insertCSS
11 ],
12 "host_permissions": [
13 "https://*/*", // required by scripting + DNR host actions
14 "http://*/*"
15 ],
16 "background": {
17 "service_worker": "sw.js",
18 "type": "module"
19 },
20 "declarative_net_request": {
21 "rule_resources": [
22 { "id": "ruleset_1", "enabled": true, "path": "rules.json" }
23 ]
24 }
25}
Execution context: Parsed by the browser at install and update time, not at runtime. Chrome and Edge treat declarativeNetRequestFeedback as an optional capability; Firefox accepts it since Manifest V3 support landed in v109; Safari silently ignores unrecognised permission strings rather than rejecting the extension.
chrome.storage is the only durable data layer available to every execution context. The service worker must treat it as its sole source of truth because all module-scope variables are wiped on eviction. The three areas differ in scope, quota, and replication behaviour: local (up to 10 MB, device-only), sync (100 KB total, replicated via the signed-in account), and session (in-memory for the browser session, cleared on restart).
1// sw.js — read on every service worker cold start
2chrome.runtime.onInstalled.addListener(async () => {
3 const { schemaVersion } = await chrome.storage.local.get("schemaVersion");
4 if (!schemaVersion || schemaVersion < 2) {
5 await chrome.storage.local.set({ schemaVersion: 2, settings: { theme: "system" } });
6 }
7});
8
9// Respond to storage changes in every context simultaneously
10chrome.storage.onChanged.addListener((changes, area) => {
11 if (area === "sync" && changes.settings) {
12 applySettings(changes.settings.newValue);
13 }
14});
Execution context: Service worker background thread. All chrome.storage methods return Promises in MV3; await is safe here. Content scripts can call chrome.storage directly if the storage permission is declared — no messaging required. Firefox uses browser.storage with native Promises; Safari maps sync onto iCloud with tighter per-extension caps.
No two extension contexts share a JavaScript heap, which means every cross-context operation requires an explicit message. One-shot sendMessage / onMessage handles request-response flows; long-lived connect / Port is better for streaming updates. Both channels use the structured-clone algorithm, so Map, Set, and class instances are not transferable.
1// content-script.js — one-shot query
2async function getBlockList(): Promise<string[]> {
3 const response = await chrome.runtime.sendMessage({ type: "GET_BLOCK_LIST" });
4 if (chrome.runtime.lastError) throw new Error(chrome.runtime.lastError.message);
5 return response.urls ?? [];
6}
7
8// sw.js — respond from the service worker
9chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
10 if (msg.type === "GET_BLOCK_LIST") {
11 chrome.storage.local.get("blockList").then(({ blockList }) => {
12 sendResponse({ urls: blockList ?? [] });
13 });
14 return true; // keep the channel open for the async response
15 }
16});
Execution context: sendMessage can originate from any context; onMessage fires in the service worker and any open extension pages. return true from the onMessage listener is mandatory whenever sendResponse is called asynchronously — omitting it closes the port before the reply arrives. Firefox handles this identically; Safari occasionally drops responses if the listener does not return true within the same microtask.
declarativeNetRequest (DNR) evaluates static and dynamic rules off the main thread — no content script or service worker is involved in per-request processing. This is both its strength (sub-millisecond overhead, no webRequest permission) and its constraint (no access to request bodies, no programmatic inspection). Static rules are declared in rules.json and compiled at install time; dynamic rules are written at runtime via updateDynamicRules and survive extension restarts.
1// sw.js — add a dynamic redirect rule at runtime
2async function blockTracker(domain: string) {
3 const id = Math.floor(Math.random() * 1_000_000);
4 await chrome.declarativeNetRequest.updateDynamicRules({
5 addRules: [
6 {
7 id,
8 priority: 10,
9 action: { type: "block" },
10 condition: {
11 urlFilter: `||${domain}`,
12 resourceTypes: ["script", "xmlhttprequest", "image"],
13 },
14 },
15 ],
16 removeRuleIds: [],
17 });
18 await chrome.storage.local.set({ [`rule_${domain}`]: id }); // persist id for later removal
19}
Execution context: updateDynamicRules runs in the service worker and must complete before the rule takes effect on subsequent navigations — there is no synchronous path. Chrome caps dynamic rules at 5 000 per extension; Firefox enforces the same limit as of v127; Safari supports DNR since Safari 16.4 but enforces a lower static rule ceiling and does not yet expose declarativeNetRequestFeedback.
chrome.tabs and chrome.windows require the tabs permission to read sensitive fields such as url and title. Omitting the permission still allows querying active-tab geometry but silently returns empty strings for those fields — a subtle bug that is hard to catch in testing. Always call chrome.tabs.query with the minimum viable filter and validate the result before reading tab.url.
1// popup.js — safely read the active tab URL
2async function getActiveTabUrl(): Promise<string | null> {
3 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
4 if (!tab?.url) return null; // undefined if tabs permission absent
5 if (tab.url.startsWith("chrome://")) return null; // extension cannot inject here
6 return tab.url;
7}
8
9// sw.js — react to navigation events
10chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
11 if (changeInfo.status === "complete" && tab.url?.startsWith("https://")) {
12 scheduleContentSync(tabId);
13 }
14});
Execution context: tabs.query is available in the service worker, popup, options page, and content scripts (with tabs permission). chrome.windows is not available in content scripts. Firefox treats the activeTab grant differently from Chrome when the popup is not open — prefer explicit tabs permission for background access. Safari limits some tab event payloads in Private Browsing windows.
Every permission in manifest.json is permanently visible to the Chrome Web Store review team and to users on the install dialog. Declare only what you need, request optional permissions at runtime where possible, and document the business reason for each host permission in your store listing.
The extension’s Content Security Policy (CSP) in MV3 disallows eval, new Function, inline event handlers, and remote script tags in extension pages by default. You cannot relax the script-src directive in MV3 — any attempt to add 'unsafe-eval' is rejected by Chrome. Audit bundler output: some transpilers emit eval for source maps or dynamic requires, which silently breaks at runtime.
For data in transit, prefer chrome.storage.local over sync for secrets and tokens. Synced data is replicated through the user’s account infrastructure, so anything written to storage.sync should be treated as potentially readable outside the device. Validate all onMessage payloads rigorously — a malicious web page can trigger chrome.runtime.sendMessage to your extension if it knows the extension ID.
Permissions that require a user gesture — activeTab, clipboardWrite — cannot be triggered from the service worker background. They must be invoked from a user-facing context such as the popup or via a chrome.action click handler. If you need the scripting permission to inject on arbitrary sites, host_permissions covering those origins must be declared; activeTab alone is insufficient for programmatic injection without a prior user gesture.
| Subsystem | Chrome / Edge | Firefox (≥ 109) | Safari (≥ 16.4) |
|---|---|---|---|
chrome.storage.local | Full support | browser.storage.local · native Promises | Full support; 10 MB default cap |
chrome.storage.sync | Syncs via Google account | Syncs via Firefox account | Maps to iCloud; stricter per-extension quota |
chrome.storage.session | Since Chrome 102 | Since Firefox 115 | Not supported as of Safari 17 |
chrome.runtime messaging | Full support | browser.runtime · Promises native | Full support |
declarativeNetRequest | Full support; 5 000 dynamic rules | Since Firefox 127; same 5 000 cap | Since Safari 16.4; lower static rule ceiling |
declarativeNetRequestFeedback | Supported | Supported since Firefox 128 | Not supported |
chrome.tabs | Full support | browser.tabs; getBrowserInfo available | Full support; Private Browsing restrictions |
chrome.windows | Full support | browser.windows | Full support |
chrome.scripting | Full support | browser.scripting since Firefox 102 | Since Safari 16 |
The guides in this section each address one subsystem in depth. Chrome Storage API & Sync covers quota management, onChanged patterns, and cross-device sync semantics. Declarative Net Request Rules walks through static and dynamic rule authoring, priority scoring, and the migration path from the legacy webRequest API. The message passing architecture guide covers one-shot and port-based messaging, error handling, and the return true pitfall. Tabs API & Window Management goes deep on permission-gated field access, lifecycle events, and multi-window patterns. Scripting API & Dynamic Injection explains executeScript, insertCSS, and the world isolation model for injected code. Finally, the cross-browser API compatibility reference consolidates divergence details, polyfill strategies, and the namespace adapter pattern for shipping a single codebase across Chrome, Firefox, and Safari.
executeScript and insertCSS patterns.Chrome vs Firefox vs Safari Manifest V3 API compatibility reference — namespace differences, Promise vs callback, storage quotas, declarativeNetRequest, and the webextension-polyfill.
Use chrome.scripting in Manifest V3 to dynamically inject JavaScript and CSS — executeScript, registerContentScripts, world isolation, and activeTab patterns.
1 topics
Persist and synchronise extension state across devices with chrome.storage.sync — quotas, async patterns, change events, encryption and cross-browser adapters for Manifest V3.
3 topics
Master chrome.declarativeNetRequest static, dynamic, and session rulesets in MV3 — rule schema, modifyHeaders, redirect, block actions, quota limits, and getMatchedRules debugging.
2 topics
Bridge popup, content script, and service worker contexts in Manifest V3 with one-time messages, long-lived ports, error boundaries, and reconnect patterns that survive worker termination.
3 topics
Control browser tabs and windows in Manifest V3: tabs.query, tabs.create, tab groups, onUpdated/onActivated events, activeTab permission, and cross-browser quirks.
2 topics