Firefox WebExtension Development
Complete reference for building, testing, and publishing browser extensions for Mozilla Firefox.
Overview
Firefox extensions use the WebExtensions API with the browser.* namespace (Promise-based natively). Firefox supports both Manifest V2 and V3, with MV2 NOT deprecated (unlike Chrome).
Key Characteristics:
- Global namespace:
browser(Promise-based) - Both MV2 and MV3 supported
- Firefox-specific APIs:
sidebarAction,userScripts,contextualIdentities,protocol_handlers - Submission via AMO (addons.mozilla.org)
Manifest Structure
Manifest V3 (Recommended, Firefox 109+)
{
"manifest_version": 3,
"name": "Extension Name",
"version": "1.0.0",
"description": "Brief description",
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "109.0"
},
"gecko_android": {
"strict_min_version": "109.0"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"96": "icons/icon-96.png"
},
"default_title": "Extension Title"
},
"content_scripts": [
{
"matches": ["https://*/*"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_idle"
}
],
"permissions": ["storage", "activeTab"],
"host_permissions": ["https://api.example.com/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
}
}
Manifest V2 (Still Supported)
{
"manifest_version": 2,
"name": "Extension Name",
"version": "1.0.0",
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "78.0"
}
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": "icons/icon-48.png"
},
"permissions": ["storage", "activeTab", "https://*/*"]
}
MV2 vs MV3 Key Differences
| Feature | MV2 | MV3 |
|---------|-----|-----|
| Toolbar button | browser_action | action |
| Background | background.scripts / page | background.service_worker |
| Host permissions | In permissions | Separate host_permissions |
| Default CSP | script-src 'self'; object-src 'self'; | script-src 'self'; upgrade-insecure-requests; |
| Request blocking | webRequest.onBeforeRequest | declarativeNetRequest |
All Manifest Keys Reference
Metadata:
name(required) - Extension nameversion(required) - Version string (e.g., "1.0.0")description- Short descriptionauthor- Author namehomepage_url- Extension homepageicons- Extension icons object
Firefox-Specific:
browser_specific_settings.gecko.id- Required for AMObrowser_specific_settings.gecko.strict_min_version- Minimum Firefox versionbrowser_specific_settings.gecko.strict_max_version- Maximum Firefox versionbrowser_specific_settings.gecko_android- Android-specific settings
Background & Scripts:
background- Service worker (MV3) or scripts/page (MV2)content_scripts- Scripts injected into pagesuserScripts- User script registration (Firefox-only)declarative_net_request- Rule-based request modification
UI Components:
action(MV3) /browser_action(MV2) - Toolbar buttonpage_action- Address bar buttonsidebar_action- Sidebar panel (Firefox-only)options_ui- Options page configurationdevtools_page- DevTools extension page
Permissions:
permissions- API permissionshost_permissions(MV3) - Host accessoptional_permissions- Optional API permissionsoptional_host_permissions- Optional host access
Other:
commands- Keyboard shortcutsomnibox- Address bar integrationweb_accessible_resources- Resources accessible from pagesprotocol_handlers- Custom protocol handlerschrome_settings_overrides- Override homepage/searchchrome_url_overrides- Override new tab/bookmarks
WebExtension APIs
Complete API Namespace List (51 APIs)
| API | Permission | Description |
|-----|------------|-------------|
| action | - | Toolbar button (MV3) |
| alarms | alarms | Schedule code execution |
| bookmarks | bookmarks | Bookmark management |
| browserAction | - | Toolbar button (MV2) |
| browserSettings | - | Browser settings |
| browsingData | browsingData | Clear browsing data |
| clipboard | clipboardWrite | Clipboard access |
| commands | - | Keyboard shortcuts |
| contentScripts | - | Register content scripts |
| contextualIdentities | contextualIdentities | Container tabs (Firefox-only) |
| cookies | cookies | Cookie management |
| declarativeNetRequest | declarativeNetRequest | Rule-based request blocking |
| devtools | devtools | DevTools integration |
| dns | dns | DNS resolution |
| downloads | downloads | Download management |
| events | - | Common event types |
| extension | - | Extension utilities |
| find | find | Find text in pages |
| history | history | Browser history |
| i18n | - | Internationalization |
| identity | identity | OAuth2 authentication |
| idle | idle | Idle state detection |
| management | management | Installed add-ons info |
| menus | menus | Context menu items |
| notifications | notifications | System notifications |
| omnibox | - | Address bar suggestions |
| pageAction | - | Address bar button (MV2) |
| permissions | - | Runtime permissions |
| pkcs11 | pkcs11 | PKCS#11 modules |
| privacy | privacy | Privacy settings |
| proxy | proxy | Request proxying |
| runtime | - | Extension runtime |
| scripting | scripting | Inject scripts/CSS (MV3) |
| search | search | Search engines |
| sessions | sessions | Closed tabs/windows |
| sidebarAction | - | Sidebar (Firefox-only) |
| storage | storage | Local/managed storage |
| tabGroups | tabGroups | Tab groups |
| tabs | tabs | Tab management |
| theme | theme | Theme API |
| topSites | topSites | Frequently visited |
| userScripts | userScripts | User scripts (Firefox-only) |
| webNavigation | webNavigation | Navigation events |
| webRequest | webRequest | Request interception |
| windows | - | Window management |
Core API Usage Patterns
Tabs API:
// Query tabs
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
// Create tab
const tab = await browser.tabs.create({ url: 'https://example.com' });
// Update tab
await browser.tabs.update(tabId, { active: true });
// Send message to tab
await browser.tabs.sendMessage(tabId, { action: 'update' });
// Execute script (MV3)
await browser.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
Storage API:
// Save data
await browser.storage.local.set({ key: 'value', settings: config });
// Get data
const { key, settings } = await browser.storage.local.get(['key', 'settings']);
// Remove data
await browser.storage.local.remove('key');
// Clear all
await browser.storage.local.clear();
// Listen for changes
browser.storage.onChanged.addListener((changes, area) => {
if (changes.key) {
console.log('Old:', changes.key.oldValue, 'New:', changes.key.newValue);
}
});
Runtime Messaging:
// Content script → Background
const response = await browser.runtime.sendMessage({ action: 'getData' });
// Background listener
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'getData') {
return Promise.resolve({ data: 'response' });
}
});
// Background → Content script
browser.tabs.sendMessage(tabId, { action: 'update' });
// External messaging (from web pages)
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (sender.id === 'allowed-extension-id') {
// Handle message
}
});
Context Menus:
// Create context menu
browser.menus.create({
id: 'my-menu',
title: 'My Menu Item',
contexts: ['selection']
});
// Handle click
browser.menus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'my-menu') {
console.log('Selected:', info.selectionText);
}
});
Alarms:
// Create alarm
browser.alarms.create('my-alarm', { delayInMinutes: 1, periodInMinutes: 5 });
// Handle alarm
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'my-alarm') {
// Do work
}
});
Notifications:
// Show notification
browser.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: 'Title',
message: 'Message'
});
Firefox-Specific APIs
Sidebar Action:
// Toggle sidebar
browser.sidebarAction.open();
browser.sidebarAction.close();
// Set panel
await browser.sidebarAction.setPanel({ panel: 'sidebar.html' });
Container Tabs (Contextual Identities):
// List containers
const containers = await browser.contextualIdentities.query({});
// Create container
const container = await browser.contextualIdentities.create({
name: 'Work',
color: 'blue',
icon: 'briefcase'
});
// Create tab in container
await browser.tabs.create({
url: 'https://work.example.com',
cookieStoreId: container.cookieStoreId
});
User Scripts:
// Register user script
await browser.userScripts.register([{
js: [{ file: 'script.js' }],
matches: ['*://example.com/*'],
runAt: 'document_start'
}]);
Security & Content Security Policy
Default CSP
MV3: script-src 'self'; upgrade-insecure-requests;
MV2: script-src 'self'; object-src 'self';
CSP Restrictions
Forbidden (causes AMO rejection):
- Remote script sources
'unsafe-inline''unsafe-eval'(except'wasm-unsafe-eval'for WebAssembly)- Data URLs for scripts
eval(),new Function(), string-based code execution
Allowed:
'self'- Scripts from extension package'wasm-unsafe-eval'- WebAssembly supporthttp://localhost:<port>- Development only (remove before submission)
Custom CSP Example
{
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
"sandbox": "sandbox allow-scripts allow-forms allow-popups; script-src 'self';"
}
}
Security Best Practices
- Package all dependencies locally - No CDN scripts
- Request minimal permissions - Use
activeTabinstead of<all_urls>when possible - Use optional_permissions - Request permissions at runtime for non-critical features
- Validate user input - Sanitize HTML before
innerHTML, validate URLs - Use HTTPS - All external requests should use HTTPS
- No obfuscated code - Must be reviewable for AMO
web-ext CLI Reference
Installation
npm install -g web-ext
Commands
Run extension (development):
web-ext run # Default Firefox
web-ext run --firefox-path /path/to/firefox # Specific Firefox
web-ext run --target firefox-android # Android
web-ext run --profile-create-new # Clean profile
web-ext run --start-url https://example.com # Start URL
web-ext run --verbose # Verbose output
web-ext run --no-reload # Disable auto-reload
Lint extension:
web-ext lint # Validate manifest and code
web-ext lint --warnings-as-errors
web-ext lint --self-hosted # Skip AMO-specific checks
Build extension:
web-ext build # Create .zip in web-ext-artifacts/
web-ext build --source-dir ./src
web-ext build --artifacts-dir ./dist
Sign extension:
web-ext sign --api-key $KEY --api-secret $SECRET
web-ext sign --channel listed # Public on AMO
web-ext sign --channel unlisted # Direct download
Other commands:
web-ext docs # Open documentation
web-ext dump-config # Show configuration
Configuration File (.web-ext-config.js)
module.exports = {
sourceDir: './src',
artifactsDir: './dist',
run: {
firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
startUrl: 'https://example.com',
pref: ['extensions.webextensions.debug=true']
},
build: {
overwriteDest: true
},
sign: {
apiKey: process.env.AMO_API_KEY,
apiSecret: process.env.AMO_API_SECRET,
channel: 'listed'
}
};
Environment Variables
WEB_EXT_API_KEY=your_key
WEB_EXT_API_SECRET=your_secret
WEB_EXT_SOURCE_DIR=./src
WEB_EXT_ARTIFACTS_DIR=./dist
WEB_EXT_FIREFOX=/path/to/firefox
AMO Submission Process
Pre-Submission Checklist
- [ ] Extension ID in
browser_specific_settings.gecko.id - [ ]
web-ext lintpasses with no errors - [ ] All permissions are necessary and documented
- [ ] Privacy policy included (if collecting data)
- [ ] No obfuscated code
- [ ] Source code available (if using build tools)
- [ ] Icons: 48x48 and 96x96 minimum
- [ ] Screenshots: minimum 464x200px
- [ ] Description is clear and accurate
Submission Steps
-
Build extension:
web-ext build -
Create developer account:
- Visit https://addons.mozilla.org/developers/
- Sign up and complete profile
-
Submit:
- Go to Developer Hub → "Submit a New Add-on"
- Upload
.zipfromweb-ext build - Choose distribution: Listed (public) or Unlisted (direct)
-
Fill listing:
- Name, description, categories
- Screenshots, icons
- Privacy policy URL (if collecting data)
- Support email/URL
-
Review process:
- Automated validation: 5-15 minutes
- Human review: 1-7 days for listed extensions
- Respond to reviewer comments promptly
Common Rejection Reasons
| Reason | Solution | |--------|----------| | Remote code execution | Package all scripts locally | | Unnecessary permissions | Remove unused permissions | | Obfuscated code | Provide source code | | Missing privacy policy | Add policy if collecting data | | Unclear functionality | Improve description | | CSP violations | Fix CSP to not allow remote scripts | | Hidden functionality | Disclose all features |
Data Collection Requirements
If extension collects/transmits user data, privacy policy must disclose:
- What data is collected
- How it's transmitted
- Purpose of collection
- User consent mechanism
- Data retention policy
Testing & Debugging
Development Workflow
# Start development
web-ext run --verbose
# Auto-reloads on file changes
# Access via about:debugging
Debugging Tools
Access debugging:
- Open
about:debugging#/runtime/this-firefox - Click "Inspect" next to extension
Debug components:
- Background: Console in debugging page
- Popup: Right-click popup → "Inspect"
- Content scripts: Regular page DevTools Console
- Options page: Right-click options → "Inspect"
Debugging Commands
// Check manifest
browser.runtime.getManifest();
// Check permissions
browser.permissions.contains({ permissions: ['tabs'] });
// Get extension URL
browser.runtime.getURL('/path/to/resource');
// Check last error
if (browser.runtime.lastError) {
console.error(browser.runtime.lastError);
}
// Reload extension programmatically
browser.runtime.reload();
Testing Strategies
Unit Testing (Jest):
import { mockBrowser } from 'webextension-pockito';
describe('Extension', () => {
beforeEach(() => mockBrowser.reset());
test('storage', async () => {
await browser.storage.local.set({ key: 'value' });
const result = await browser.storage.local.get('key');
expect(result.key).toBe('value');
});
});
Integration Testing (Selenium):
const { Builder } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
const driver = await new Builder()
.forBrowser('firefox')
.setFirefoxOptions(new firefox.Options()
.setPreference('extensions.autoDisableScopes', 0))
.build();
Best Practices
Background Service Worker (MV3)
// Keep service worker alive briefly
let keepAlive;
browser.runtime.onMessage.addListener((msg) => {
clearTimeout(keepAlive);
keepAlive = setTimeout(() => {}, 25000);
return handleMessage(msg);
});
Error Handling
async function safeAsync(fn) {
try {
return await fn();
} catch (error) {
console.error('Error:', error);
return { error: error.message };
}
}
Cross-Browser Compatibility
import browser from 'webextension-polyfill';
// Feature detection
if (browser.sidebarAction) {
// Firefox-specific
browser.sidebarAction.open();
}
// Fallback pattern
const action = browser.action || browser.browserAction;
action.setPopup({ popup: 'popup.html' });
Performance
- Use
browser.tabs.query()with specific filters - Debounce frequent operations
- Cache storage API results
- Use
alarmsAPI instead ofsetIntervalin background
Troubleshooting
Common Issues
| Error | Cause | Solution |
|-------|-------|----------|
| browser is not defined | Script not in extension context | Check manifest script paths |
| Permission denied | Missing permission | Add to manifest permissions |
| CSP violation | Remote script | Move script to extension package |
| Service worker terminated | Long operation | Use alarms API, avoid blocking |
| webRequest not blocking | MV3 limitation | Use declarativeNetRequest |
| ID mismatch | Different IDs | Set consistent ID in manifest |
Debug Commands
# Validate manifest
web-ext lint --verbose
# Run with debug prefs
web-ext run --pref extensions.webextensions.debug=true
# Test in clean profile
web-ext run --profile-create-new
# Check specific Firefox
web-ext run --firefox /path/to/firefox-beta
Cross-Browser with webextension-polyfill
Installation
npm install webextension-polyfill
Usage
// ES modules
import browser from 'webextension-polyfill';
// CommonJS
const browser = require('webextension-polyfill');
// Now browser.* works in Chrome with promises
const tabs = await browser.tabs.query({ active: true });
Manifest Setup
{
"background": {
"scripts": ["browser-polyfill.js", "background.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["browser-polyfill.js", "content.js"]
}]
}
TypeScript Support
npm install @types/webextension-polyfill
import browser from 'webextension-polyfill';
async function getActiveTab(): Promise<browser.Tabs.Tab> {
const [tab] = await browser.tabs.query({ active: true });
return tab;
}
File Structure Template
my-extension/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.js
├── options.html
├── options.js
├── browser-polyfill.js
├── styles/
│ ├── popup.css
│ └── content.css
├── icons/
│ ├── icon-16.png
│ ├── icon-32.png
│ ├── icon-48.png
│ └── icon-96.png
├── _locales/
│ ├── en/
│ │ └── messages.json
│ └── it/
│ └── messages.json
├── .web-ext-config.js
├── package.json
└── README.md