Stack Guide

How to build a browser extension that ships everywhere

Extensions are the last corner of web development where the platform actively fights you: an ephemeral background worker, four stores with four opinions, and a permissions model where asking for one word too many costs you two weeks in review. This guide picks the stack that removes most of that, then explains the parts nobody can remove for you.

About 6 min read. Recommendations verified October 2026.

A glowing puzzle-piece extension docking into a browser toolbar, built for several browsers on a bench

Manifest V3 realities that break naive code

Manifest V3 is now the only game in Chrome, and its constraints are not stylistic preferences - they will silently break an extension that was written like a web page.

The background is a service worker, and it dies. It terminates after a short idle period and restarts on the next event with a fresh global scope. Any variable you set at the top level is gone. Persist state in the storage API, replace setTimeout and setInterval with the alarms API, and write every handler so that a cold start is the normal case rather than the exception.

No remote code, ever. Loading a script from a CDN, evaluating a string, or shipping an interpreter for downloaded logic is an automatic rejection. Everything executable lives in your package. This is also why a bundler that inlines dependencies is not optional.

Permissions are the review. Ask for the narrowest set that works: activeTab instead of host permissions on every site, optional_host_permissions requested at the moment the user triggers the feature, and scripting only if you actually inject. Requesting permissions your code never calls is the single most common rejection reason, and broad host access is what turns a two-day review into a three-week one.

No DOM in the worker. Service workers have no document, so parsing HTML, using canvas, or playing audio needs an offscreen document. Firefox's background pages behave differently again, which is exactly the kind of divergence a framework absorbs for you.

Content script and background patterns worth copying

One typed message contract

Define a single discriminated union of message types in a shared module and import it in the content script, the background, and the popup. Every cross-context call goes through one helper that returns a typed promise. It costs an hour and eliminates the entire category of silent string-mismatch bugs.

Content scripts do DOM, background does network

Keep fetches, credentials, and API keys in the background worker where the page cannot see them, and let the content script handle only what needs the document. It also sidesteps most cross-origin problems, because the worker is not bound by the host page's origin.

Know which world you are in

Content scripts run in an isolated world by default, so they see the DOM but not the page's JavaScript variables. Reaching page globals means an explicit main-world script and message passing back. Guessing wrong here produces code that works in your test page and nowhere else.

Survive single-page navigation

On sites that never do a full page load, your injected UI needs to re-mount on route changes and clean up after itself. WXT's content script context gives you an invalidation signal; without one you leak observers and end up with three copies of your button.

Surviving store review on Chrome and Firefox

Review timing in 2026 is roughly: updates to an established listing in 24-48 hours, a new extension from an established account in two to five business days, and a first submission from a brand-new developer account in one to two weeks. Broad host permissions or sensitive APIs routinely push that to three weeks. Plan launch dates around the slow path.

  • Write the justifications like a human will read them. Chrome asks you to justify each permission and the single purpose of the extension. Vague answers get bounced; one plain sentence naming the feature that needs it does not.
  • Publish a real privacy policy at a real URL. Required if you touch user data at all, and a missing or generic one is a top rejection cause. Say what you collect, where it goes, and how long you keep it.
  • Give AMO your source. Firefox requires a source archive plus build instructions whenever the submitted code is bundled or minified, which is always with a modern toolchain. Automate it in the same job that builds the zip.
  • Ship the small version first. Get an approved listing with a narrow permission set, then expand. Reviewers treat an established, clean listing very differently from a first submission asking for access to every site.
  • Automate submission. Wire the build, zip, and upload for all three stores into a release job so a tag pushes everywhere. The GitHub Actions guide covers the workflow shape, and store API keys belong in encrypted secrets.

Browser extension questions, answered

Which browser extension framework should I use in 2026?

WXT, for almost every new project. It is actively maintained, it is built on Vite so builds and hot reload are fast, and it produces per-browser bundles from one codebase. CRXJS is the better runner-up if you want a Vite plugin rather than a framework - it shipped 3.0 in September 2026 - while Plasmo has had no release since May 2025, so treat it as stalled for new work.

Can one codebase really target Chrome, Firefox, Edge, and Safari?

Mostly. Chrome and Edge are the same target in practice, and WXT handles the Firefox manifest differences for you, so those three come from one source tree with a build flag. Safari is the outlier: you convert the built extension with Apple's converter tool, then sign and distribute it through the App Store with an Apple developer account. Budget separate testing time for Safari rather than assuming parity.

Why does my Manifest V3 background script keep losing state?

Because it is a service worker, not a persistent page. Chrome terminates it after a short idle period and restarts it on the next event with a completely fresh global scope, so any variable held in module scope disappears. Persist anything that must survive into the storage API, replace timers with the alarms API, and treat every event handler as though the worker just cold-started, because most of the time it did.

How long does Chrome Web Store review take?

Updates to an existing listing usually clear in 24-48 hours. A new extension from an established developer account takes two to five business days, and a first submission from a brand-new account can take one to two weeks while Google evaluates account trust. Anything requesting broad host permissions or sensitive APIs regularly stretches to three weeks, so narrow your permissions before you plan a launch date.

Where to go next

Choose what renders in the popup over in the frontend directory, and set up the browser tests with the testing tools guide. Most extensions eventually need a small backend for sync or licensing - the API stack guide covers that, and the TypeScript cheatsheet is handy while you are typing those message contracts. Read the official docs at wxt.dev.