Manifest V3 background service_worker with type: "module"
Chromium uses a service worker for the MV3 background. To import ES modules from it, the manifest needs type: "module":
"type": "module", import statements fail at registration with no clear error in the extension console. With it, you can write modern ES module syntax in the worker file.
Extension.js sets type: "module" automatically when your background entry uses ES module syntax, and it compiles .ts/.tsx workers to a single bundled output so module resolution still works in production.
Why background.js behaves differently in Manifest V3
In Manifest V2, background.js ran in a persistent background page with a DOM. In Manifest V3 on Chromium, the background runs as a service worker:
- No DOM.
window,document,XMLHttpRequest, andlocalStorageare gone. Usefetchandchrome.storage. - The worker can be terminated at any time when idle and woken on the next event. Do not store state in module-scope variables; persist it in
chrome.storage. - Top-level
awaitis allowed, but long initialization will not keep the worker alive on its own. - Register event listeners synchronously at the top of the file, not inside async callbacks. Listeners registered inside async callbacks will not fire on the wake-up event that loaded the worker.
service_worker and Firefox to scripts from the same source, so the same background entry compiles correctly per target.
web_accessible_resources in Manifest V3
Manifest V3 changed web_accessible_resources from a flat array of files to a list of { resources, matches } blocks:
- Listing a path that is not in the build output. Extension.js only emits files that are referenced from
manifest.json, the entry HTML, or imported code. Add the file as an asset so it lands indist/<browser>. - Forgetting
matches. Without it, the resource is not exposed to any origin. - Using V2-style flat strings. The browser silently ignores them in MV3.
web_accessible_resources implementation for the full pattern, including injecting an extension URL into a content script.
host_permissions vs permissions
Manifest V3 split host access out of the permissions array:
| Key | What it controls |
|---|---|
permissions | Named API surfaces (storage, tabs, cookies, scripting, alarms, etc.). |
host_permissions | URL match patterns the extension can read or modify (https://*/*, *://api.example.com/*). |
optional_permissions and optional_host_permissions | Permissions requested at runtime via chrome.permissions.request. |
chrome.cookies.getreturning empty for a site you have access to: the URL needshost_permissions, not justcookies.chrome.scripting.executeScriptfailing with āCannot access contents of urlā: add the URL tohost_permissions.- Web store warning users about āall-siteā access when you only need one origin: narrow the host pattern.
declarative_net_request in Firefox
declarative_net_request (DNR) is the MV3 replacement for blocking webRequest. Firefox supports it, but with a few constraints:
- Static rule resource files (
rule_resources) must be valid JSON arrays. Firefox is stricter about empty or malformed rule files than Chrome. - Some rule actions and conditions Chrome supports are still partial in Firefox. Check MDNās
declarativeNetRequestreference for the current matrix. host_permissions(or<all_urls>) is required for DNR rules to apply on cross-origin requests.
Content scripts, workers, and extension URLs
Three places where MV3 trips people up:- Content scripts cannot access the pageās JavaScript scope. They share the DOM, not the window. Use
window.postMessage(or a<script>tag pointing to aweb_accessible_resourcesentry) to communicate with page code. - Service workers cannot reach
chrome.runtime.getURLreliably during install. Resolve URLs lazily, inside the event handler that needs them. - Extension URLs are origin-scoped. Two pages from your extension can talk to each other, but you cannot fetch them from the page unless they are listed in
web_accessible_resources.
How Extension.js helps with Manifest V3
- Routes background entries to
service_worker(Chromium) orscripts(Firefox) automatically. - Sets
type: "module"when the background uses ES module syntax. - Validates
web_accessible_resources,permissions, andhost_permissionsagainst the active manifest version at build time. - Emits per-browser
dist/<browser>artifacts so MV3 differences between Chrome and Firefox stay isolated. - Reloads content scripts, the service worker, and HTML pages on save during
extension dev.
Next steps
- Read Permissions and host permissions.
- Read Background scripts and Content scripts.
- Read
web_accessible_resources. - See Cross-browser compatibility for the build pipeline.
- See Browser-specific manifest fields for prefixed manifest keys.

