Back to skills
SKILL.md
Browser Extension Project Architect
ASecurityProduction-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.
- 9 stars
- 0 votes
- 0 copies
- 0 views
- Added September 8, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add anubhavg-icpl/vibe --skill browser-extension-project-architect --agent claude-codeAre you the author of Browser Extension Project Architect?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-browser-extension-project-architect)---
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)
Attribution
Comments
Loading commentsā¦
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.