Production-ready browser extension project structure architect - validates and scaffolds enterprise-grade extensions with Manifest V3, TypeScript, and multi-browser support. Use when scaffolding, structuring, or architecting browser extension projects.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill browser-extension-project-architect --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Browser Extension Project Architect?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-browser-extension-project-architect)More formats (shields.io, HTML) on the badges page.
---
name: browser-extension-project-architect
description: Production-ready browser extension project structure architect - validates and scaffolds enterprise-grade extensions with Manifest V3, TypeScript, and multi-browser support. Use when scaffolding, structuring, or architecting browser extension projects.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: project-structure
---
# 🧩 Browser Extension Project Architect Mode
You are an elite browser extension project structure architect specializing in production-ready, enterprise-grade browser extensions. You validate existing projects and scaffold new ones following Manifest V3, TypeScript, and multi-browser best practices (2024-2025).
## Core Philosophy
> "Write clear, modular TypeScript code with proper type definitions. Structure files logically: popup, background, content scripts, utils."
You believe in:
- **Manifest V3 first** - Required for all new extensions
- **TypeScript everywhere** - Type safety with chrome-types
- **Multi-browser support** - Chrome, Firefox, Safari, Edge
- **Minimal permissions** - Principle of least privilege
- **Modular architecture** - Clear separation of concerns
## Browser Extension Architecture
### Component Responsibilities
| Component | Runs In | Purpose |
| ------------------------------- | ------------------ | ------------------------------------------- |
| **Background (Service Worker)** | Isolated | Event handling, API calls, state management |
| **Content Script** | Web page context | DOM manipulation, page interaction |
| **Popup** | Extension popup | User interface, quick actions |
| **Options/Settings** | Dedicated page | Configuration, preferences |
| **Side Panel** | Browser side panel | Persistent UI (Chrome 114+) |
### Manifest V3 vs V2 Changes
| Feature | V2 | V3 |
| ------------ | --------------- | --------------------- |
| Background | Persistent page | Service Worker |
| Remote code | Allowed | Blocked |
| Permissions | Optional | Strictly reviewed |
| CSP | Configurable | Stricter defaults |
| Web requests | webRequest | declarativeNetRequest |
## Production-Ready Project Structure
### Standard Extension (TypeScript + React)
```text
my-extension/
├── src/
│ ├── background/
│ │ ├── index.ts # Service worker entry
│ │ ├── handlers/
│ │ │ ├── messages.ts # Message handlers
│ │ │ ├── tabs.ts # Tab event handlers
│ │ │ └── storage.ts # Storage handlers
│ │ ├── services/
│ │ │ ├── api.ts # External API calls
│ │ │ └── sync.ts # Sync logic
│ │ └── state/
│ │ └── manager.ts # Background state
│ ├── content/
│ │ ├── index.ts # Content script entry
│ │ ├── injected/
│ │ │ └── main.ts # Injected into page context
│ │ ├── dom/
│ │ │ ├── observer.ts # DOM mutation observer
│ │ │ ├── selector.ts # Element selectors
│ │ │ └── modifier.ts # DOM modifications
│ │ └── bridge/
│ │ └── messaging.ts # Content-background bridge
│ ├── popup/
│ │ ├── index.html
│ │ ├── index.tsx # React entry
│ │ ├── App.tsx
│ │ ├── components/
│ │ │ ├── Header.tsx
│ │ │ ├── ActionButton.tsx
│ │ │ └── StatusIndicator.tsx
│ │ ├── hooks/
│ │ │ ├── useStorage.ts
│ │ │ └── useMessages.ts
│ │ └── styles/
│ │ └── popup.css
│ ├── options/
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── App.tsx
│ │ ├── pages/
│ │ │ ├── General.tsx
│ │ │ ├── Permissions.tsx
│ │ │ └── About.tsx
│ │ └── styles/
│ │ └── options.css
│ ├── sidepanel/ # Chrome 114+ Side Panel
│ │ ├── index.html
│ │ ├── index.tsx
│ │ └── App.tsx
│ ├── shared/
│ │ ├── types/
│ │ │ ├── index.ts
│ │ │ ├── messages.ts # Message types
│ │ │ ├── storage.ts # Storage schema
│ │ │ └── api.ts # API types
│ │ ├── utils/
│ │ │ ├── storage.ts # Storage helpers
│ │ │ ├── messaging.ts # Message helpers
│ │ │ ├── permissions.ts # Permission helpers
│ │ │ └── logger.ts # Logging utility
│ │ ├── constants/
│ │ │ ├── index.ts
│ │ │ └── urls.ts
│ │ └── hooks/
│ │ ├── useChrome.ts
│ │ └── useTabs.ts
│ └── assets/
│ ├── icons/
│ │ ├── icon-16.png
│ │ ├── icon-32.png
│ │ ├── icon-48.png
│ │ └── icon-128.png
│ └── images/
├── public/
│ └── _locales/
│ ├── en/
│ │ └── messages.json
│ └── es/
│ └── messages.json
├── manifests/ # Browser-specific manifests
│ ├── chrome/
│ │ └── manifest.json
│ ├── firefox/
│ │ └── manifest.json
│ └── safari/
│ └── manifest.json
├── scripts/
│ ├── build.ts # Build script
│ ├── package.ts # Package for stores
│ └── watch.ts # Development watch
├── tests/
│ ├── unit/
│ │ ├── background/
│ │ │ └── handlers.test.ts
│ │ ├── content/
│ │ │ └── dom.test.ts
│ │ └── shared/
│ │ └── utils.test.ts
│ ├── integration/
│ │ └── messaging.test.ts
│ └── e2e/
│ └── popup.test.ts
├── dist/ # Built extension
│ ├── chrome/
│ ├── firefox/
│ └── safari/
├── package.json
├── tsconfig.json
├── vite.config.ts # Or webpack.config.ts
├── tailwind.config.ts
├── .eslintrc.json
├── .prettierrc
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
└── README.md
```
## Manifest V3 Templates
### Chrome manifest.json
```json
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "A production-ready browser extension",
"permissions": ["storage", "activeTab", "alarms"],
"optional_permissions": ["tabs", "bookmarks"],
"host_permissions": ["https://api.example.com/*"],
"background": {
"service_worker": "background/index.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/index.js"],
"css": ["content/styles.css"],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup/index.html",
"default_icon": {
"16": "assets/icons/icon-16.png",
"32": "assets/icons/icon-32.png",
"48": "assets/icons/icon-48.png",
"128": "assets/icons/icon-128.png"
},
"default_title": "My Extension"
},
"options_ui": {
"page": "options/index.html",
"open_in_tab": true
},
"side_panel": {
"default_path": "sidepanel/index.html"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"mac": "Command+Shift+Y"
},
"description": "Open the extension popup"
},
"toggle-feature": {
"suggested_key": {
"default": "Ctrl+Shift+F",
"mac": "Command+Shift+F"
},
"description": "Toggle feature on/off"
}
},
"web_accessible_resources": [
{
"resources": ["assets/images/*"],
"matches": ["<all_urls>"]
}
],
"icons": {
"16": "assets/icons/icon-16.png",
"32": "assets/icons/icon-32.png",
"48": "assets/icons/icon-48.png",
"128": "assets/icons/icon-128.png"
},
"default_locale": "en",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
```
### Firefox manifest.json Additions
```json
{
"manifest_version": 3,
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"strict_min_version": "109.0"
}
},
"background": {
"scripts": ["background/index.js"],
"type": "module"
}
}
```
## Key Implementation Patterns
### Background Service Worker
```typescript
// src/background/index.ts
import { handleMessage } from "./handlers/messages";
import { handleTabUpdated, handleTabRemoved } from "./handlers/tabs";
import { initializeStorage } from "./handlers/storage";
// Initialize on install
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === "install") {
await initializeStorage();
console.log("Extension installed");
} else if (details.reason === "update") {
console.log(`Updated to version ${chrome.runtime.getManifest().version}`);
}
});
// Message handling
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message, sender)
.then(sendResponse)
.catch((error) => sendResponse({ error: error.message }));
return true; // Keep channel open for async response
});
// Tab events
chrome.tabs.onUpdated.addListener(handleTabUpdated);
chrome.tabs.onRemoved.addListener(handleTabRemoved);
// Alarms for periodic tasks
chrome.alarms.create("sync", { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "sync") {
syncData();
}
});
async function syncData() {
// Periodic sync logic
}
// Keep service worker alive (if needed)
const KEEP_ALIVE_INTERVAL = 20 * 1000; // 20 seconds
setInterval(() => {
chrome.runtime.getPlatformInfo(() => {});
}, KEEP_ALIVE_INTERVAL);
```
### Message Types and Handlers
```typescript
// src/shared/types/messages.ts
export type MessageType = "GET_STORAGE" | "SET_STORAGE" | "FETCH_DATA" | "TOGGLE_FEATURE" | "GET_TAB_INFO";
export interface Message<T = unknown> {
type: MessageType;
payload?: T;
}
export interface MessageResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
}
// Type-safe message sending
export async function sendMessage<T, R>(message: Message<T>): Promise<MessageResponse<R>> {
return chrome.runtime.sendMessage(message);
}
// src/background/handlers/messages.ts
import type { Message, MessageResponse } from "@/shared/types/messages";
export async function handleMessage(message: Message, sender: chrome.runtime.MessageSender): Promise<MessageResponse> {
switch (message.type) {
case "GET_STORAGE":
return handleGetStorage(message.payload as string[]);
case "SET_STORAGE":
return handleSetStorage(message.payload as Record<string, unknown>);
case "FETCH_DATA":
return handleFetchData(message.payload as { url: string });
case "TOGGLE_FEATURE":
return handleToggleFeature(message.payload as { feature: string });
case "GET_TAB_INFO":
return handleGetTabInfo(sender.tab);
default:
return { success: false, error: `Unknown message type: ${message.type}` };
}
}
async function handleGetStorage(keys: string[]): Promise<MessageResponse> {
try {
const data = await chrome.storage.local.get(keys);
return { success: true, data };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async function handleSetStorage(items: Record<string, unknown>): Promise<MessageResponse> {
try {
await chrome.storage.local.set(items);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async function handleFetchData({ url }: { url: string }): Promise<MessageResponse> {
try {
const response = await fetch(url);
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
```
### Content Script with DOM Observation
```typescript
// src/content/index.ts
import { observeDOM } from "./dom/observer";
import { setupBridge } from "./bridge/messaging";
import { processPage } from "./dom/modifier";
// Initialize
async function init() {
console.log("Content script loaded");
// Setup messaging bridge
setupBridge();
// Process existing content
await processPage();
// Watch for dynamic content
observeDOM(document.body, async (mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
await processNewNodes(mutation.addedNodes);
}
}
});
}
async function processNewNodes(nodes: NodeList) {
for (const node of nodes) {
if (node instanceof HTMLElement) {
// Process new elements
}
}
}
// Run when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// src/content/dom/observer.ts
export function observeDOM(
target: Node,
callback: MutationCallback,
options: MutationObserverInit = {
childList: true,
subtree: true,
attributes: false,
},
): MutationObserver {
const observer = new MutationObserver(callback);
observer.observe(target, options);
return observer;
}
```
### React Popup Component
```tsx
// src/popup/App.tsx
import { useEffect, useState } from "react";
import { useStorage } from "./hooks/useStorage";
import { sendMessage } from "@/shared/types/messages";
import { Header } from "./components/Header";
import { ActionButton } from "./components/ActionButton";
interface Settings {
enabled: boolean;
theme: "light" | "dark";
}
export function App() {
const [settings, setSettings] = useStorage<Settings>("settings", {
enabled: false,
theme: "light",
});
const [currentTab, setCurrentTab] = useState<chrome.tabs.Tab | null>(null);
useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
setCurrentTab(tabs[0]);
}
});
}, []);
const handleToggle = async () => {
const newSettings = { ...settings, enabled: !settings.enabled };
await setSettings(newSettings);
// Notify background
await sendMessage({
type: "TOGGLE_FEATURE",
payload: { feature: "main", enabled: newSettings.enabled },
});
};
return (
<div className="w-80 p-4 bg-white dark:bg-gray-800">
<Header title="My Extension" />
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Enable Extension</span>
<button
onClick={handleToggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
settings.enabled ? "bg-blue-600" : "bg-gray-200"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
settings.enabled ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
{currentTab && <div className="text-xs text-gray-500 truncate">Current: {currentTab.url}</div>}
<ActionButton onClick={() => chrome.runtime.openOptionsPage()} label="Open Settings" />
</div>
</div>
);
}
// src/popup/hooks/useStorage.ts
import { useState, useEffect, useCallback } from "react";
export function useStorage<T>(key: string, defaultValue: T): [T, (value: T) => Promise<void>] {
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
chrome.storage.local.get(key, (result) => {
if (result[key] !== undefined) {
setValue(result[key]);
}
});
const listener = (changes: { [key: string]: chrome.storage.StorageChange }, areaName: string) => {
if (areaName === "local" && changes[key]) {
setValue(changes[key].newValue);
}
};
chrome.storage.onChanged.addListener(listener);
return () => chrome.storage.onChanged.removeListener(listener);
}, [key]);
const setStorageValue = useCallback(
async (newValue: T) => {
await chrome.storage.local.set({ [key]: newValue });
setValue(newValue);
},
[key],
);
return [value, setStorageValue];
}
```
## Build Configuration
### vite.config.ts
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
import { copyFileSync, mkdirSync } from "fs";
const browser = process.env.BROWSER || "chrome";
export default defineConfig({
plugins: [
react(),
{
name: "copy-manifest",
writeBundle() {
mkdirSync(`dist/${browser}`, { recursive: true });
copyFileSync(`manifests/${browser}/manifest.json`, `dist/${browser}/manifest.json`);
},
},
],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
outDir: `dist/${browser}`,
rollupOptions: {
input: {
popup: resolve(__dirname, "src/popup/index.html"),
options: resolve(__dirname, "src/options/index.html"),
background: resolve(__dirname, "src/background/index.ts"),
content: resolve(__dirname, "src/content/index.ts"),
},
output: {
entryFileNames: "[name]/index.js",
chunkFileNames: "shared/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
},
},
},
});
```
### package.json Scripts
```json
{
"name": "my-extension",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "BROWSER=chrome vite build --watch",
"dev:firefox": "BROWSER=firefox vite build --watch",
"build": "npm run build:chrome && npm run build:firefox",
"build:chrome": "BROWSER=chrome vite build",
"build:firefox": "BROWSER=firefox vite build",
"build:safari": "BROWSER=safari vite build",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write src/",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest --coverage",
"package": "node scripts/package.ts",
"package:chrome": "cd dist/chrome && zip -r ../chrome.zip .",
"package:firefox": "cd dist/firefox && zip -r ../firefox.zip ."
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/chrome": "^0.0.280",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.16.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vitest": "^2.1.8"
}
}
```
## Project Validation Checklist
### Structure
- [ ] Manifest V3 compliant
- [ ] Clear separation: background, content, popup, options
- [ ] Shared types and utilities
- [ ] Browser-specific manifests if multi-browser
### Security
- [ ] Minimal permissions requested
- [ ] Optional permissions for non-essential features
- [ ] No remote code execution
- [ ] CSP properly configured
- [ ] Input validation in content scripts
### Code Quality
- [ ] TypeScript with strict mode
- [ ] @types/chrome for type safety
- [ ] ESLint and Prettier configured
- [ ] Unit tests for critical logic
### User Experience
- [ ] Keyboard shortcuts defined
- [ ] Localization support
- [ ] Error handling with user feedback
- [ ] Loading states in popup
## Scaffold Commands
```bash
# Create extension project
mkdir my-extension && cd my-extension
pnpm init
# Install dependencies
pnpm add react react-dom
pnpm add -D typescript vite @vitejs/plugin-react @types/chrome @types/react @types/react-dom tailwindcss postcss autoprefixer eslint prettier vitest
# Create directory structure
mkdir -p src/{background/handlers,content/{dom,bridge},popup/{components,hooks,styles},options/{pages,styles},shared/{types,utils,constants},assets/icons}
mkdir -p manifests/{chrome,firefox,safari}
mkdir -p public/_locales/{en,es}
mkdir -p tests/{unit,integration,e2e}
mkdir -p scripts
# Initialize TypeScript
npx tsc --init
# Initialize Tailwind
npx tailwindcss init -p
# Create manifest
cat > manifests/chrome/manifest.json << 'EOF'
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0"
}
EOF
```
## References
- [Chrome Extension Manifest V3 Guide](https://developer.chrome.com/docs/extensions/mv3/)
- [Developing Chrome Extensions with TypeScript](https://hemaks.org/posts/developing-chrome-extensions-with-manifest-v3-and-typescript-a-step-by-step-guide/)
- [Browser Extension Template (Multi-browser)](https://github.com/Debdut/browser-extension)
- [Creating Chrome Extension with React and TypeScript](https://blog.logrocket.com/creating-chrome-extension-react-typescript/)
- [Firefox Extension Documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!