Building a production-grade browser extension today looks nothing like the Manifest V2 days. The platform is stricter, the runtime model has changed, and the surface area has grown. A modern extension needs proper architecture, sane tooling, and a clear picture of how its different contexts talk to each other.
This playbook walks through the entire lifecycle, from idea to deployment to long-term maintenance, with the practical defaults that hold up in real projects.
Phase 0: Foundation and ideation
Before writing any code, get clear on what the extension is for and how the user will interact with it.
1. Define the core problem
Be specific. Vague problem statements lead to feature creep.
- Vague: "Helps users save articles."
- Precise: "Saves the active tab's article to a backend read-later service with one click and a keyboard shortcut."
The second version tells you what UI you need, what permissions you need, and what API you need. The first version tells you nothing.
2. Choose the extension type
Extensions can expose several UX surfaces. Pick deliberately:
- Popup (Browser Action). The toolbar icon opens a small UI.
- Page Action. Largely deprecated in MV3, appears only on specific pages.
- Content Script. Injected into web pages, reads or modifies DOM.
- Background Script (Service Worker). Event-driven, handles permissions, state, and orchestration.
- Options Page. A full settings page.
- DevTools Panel. Custom tools integrated into browser DevTools.
- Override Pages. Replace the New Tab page, history page, or bookmarks page.
- Offscreen Document. A hidden page used by the service worker when it needs DOM APIs.
You will usually combine two or three of these.
3. Map the user flow
Sketch the entire path before you write code. Install, grant permissions, open popup, trigger action, view result. This single sketch will dictate your architecture and your data model.
4. Research the competition
Audit extensions that solve adjacent problems. Identify gaps and friction points users complain about in reviews. Do not copy, out-execute.
Phase 1: Choose the right stack
Hand-rolled JavaScript and manual zip uploads are how extensions become unmaintainable. A modern extension deserves a modern stack.
Recommended stack
| Category |
Recommendation |
Why |
| Build tool |
Vite |
Fast HMR, predictable output, minimal config. |
| Framework |
React, Vue, or Svelte |
Component-based UI you already know. |
| Vite plugin |
@crxjs/vite-plugin |
Handles MV3 manifest, HMR for content scripts, bundling. |
| Language |
TypeScript |
Non-negotiable for anything that has to live longer than a weekend. |
| Browser API |
webextension-polyfill |
One API across Chrome, Firefox, Edge, and Safari. |
| Styling |
Tailwind CSS |
Fast UI iteration without CSS file sprawl. |
| Components |
shadcn/ui or Radix |
Accessible primitives for popups and options pages. |
Recommended project structure
my-extension/
├── public/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── src/
│ ├── background/ # Service Worker
│ ├── content/ # Content Scripts
│ ├── popup/ # Popup UI
│ ├── options/ # Settings page
│ ├── components/ # Shared UI
│ ├── lib/ # Storage, API, helpers
│ └── manifest.json
├── package.json
└── vite.config.ts
This layout scales as you add more surfaces (DevTools panel, offscreen documents, side panels). Flat layouts collapse fast.
Phase 2: Architecture and development
1. Manifest V3 is mandatory
The differences from V2 that matter day to day:
- Background pages are now service workers, with a lifecycle that can suspend and resume.
- No remote-code execution. Everything must be bundled.
- Stricter Content Security Policy.
- A unified
action API replaces browserAction and pageAction.
- Offscreen documents are required for any DOM-bound work the service worker needs to do.
Designing as if these constraints apply from day one is much cheaper than refactoring later.
2. Master extension communication
Each part of the extension runs in its own isolated world. Messages cross the boundaries explicitly.
| From |
To |
Method |
Purpose |
| Popup |
Background |
runtime.sendMessage |
Trigger actions, read state. |
| Background |
Popup |
onMessage reply |
Push updates back. |
| Content Script |
Background |
runtime.sendMessage |
Share page data. |
| Background |
Content Script |
tabs.sendMessage |
Inject commands. |
| Page script |
Content script |
window.postMessage |
Cross the isolated-world boundary. |
Misunderstanding this model is the single largest source of "it works locally and breaks in production" bugs.
3. Offscreen documents
Background service workers cannot use DOM APIs, so tasks like MediaRecorder, HTML parsing, canvas operations, and audio processing have to run in an offscreen document.
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['USER_MEDIA'],
justification: 'Record audio from microphone',
});
If you are building anything in the recorder, converter, or media-processing space, this is the API that makes it possible.
Phase 3: Build and deployment
1. Local development with Vite and CRXJS
Standard workflow:
- Run
npm run dev.
- Open
chrome://extensions.
- Enable Developer Mode.
- Load the
dist folder as an unpacked extension.
CRXJS provides HMR even for content scripts, which most toolchains break.
2. Publishing to the Chrome Web Store
- Create a developer account and pay the one-time fee.
- Upload a zipped
dist folder.
- Add screenshots, description, and a privacy policy.
- Submit for review.
Approval can take hours or days depending on the permissions you request. Aggressive permissions (broad host access, tabs, history) extend review time.
3. Publishing to Firefox Add-ons (AMO)
If you use webextension-polyfill, most code works without modification. Firefox reviews are stricter than Chrome's but usually faster.
4. CI/CD with GitHub Actions
A workflow that pays for itself within a month:
- On push: lint, test, build.
- On release tag: build, zip, attach to a GitHub Release.
- Optional: auto-upload to Chrome and Firefox beta channels.
Automate everything except the typing.
Phase 4: Testing and quality assurance
1. Unit testing
Use Vitest or Jest for:
- API helpers
- Storage wrappers
- Pure logic functions
2. End-to-end testing
Use Playwright or Cypress to:
- Launch a real browser with the extension loaded
- Drive the popup UI
- Exercise content scripts on real pages
- Verify background-script side effects
Few extensions get tested properly. That is why so many of them break on browser updates.
3. Manual checklist
- Chrome, Firefox, Edge
- Windows, macOS, Linux
- Install, upgrade, and reinstall flows
- Behaviour with other popular extensions installed alongside
- Permission prompts
- Offline mode
Phase 5: Post-launch maintenance
Launching is the midpoint, not the finish line.
1. Error tracking
Use Sentry (or similar) to capture:
- Runtime errors
- Unhandled promise rejections
- Background script failures
You will surface issues users never bother to report.
2. Privacy-friendly analytics
Plausible or Fathom work well. Avoid Google Analytics: heavy, slow, and a poor fit for an extension's sandbox.
3. Feedback loop
Provide a link to GitHub Issues or a lightweight feedback form. Users who can complain usefully are worth more than users who silently uninstall.
4. Regular updates
Browsers ship changes constantly. Update your dependencies and your manifest features on a schedule, not only when something breaks.
Conclusion
A modern browser extension is, in effect, a multi-surface web application with strict sandboxing and an event-driven runtime. If you plan deliberately, pick the right tools, respect Manifest V3's constraints, and automate your pipeline, you can ship stable, scalable extensions with far less friction than the platform's reputation suggests.
The playbook above is the version I wish someone had handed me before I started. Use it, skip the early mistakes, and spend your energy on the parts of the product that actually matter to users.