DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Building Firefox Extensions: WebExtensions API, Firefox-Only Features, and AMO Submission

You want to build a Firefox extension. Where do you start, what's different from Chrome, and what do you need to know about Firefox-specific APIs? Here's a practical guide.

WebExtensions: The Shared Standard

Firefox uses the WebExtensions API, which is largely compatible with Chrome extensions. An extension built for Chrome can often be ported to Firefox with minor changes.

But Firefox has important differences:

  1. Manifest V2 support: Firefox still supports MV2 indefinitely. Chrome is deprecating it.
  2. Browser namespace: Firefox uses browser.* (with Promises); Chrome uses chrome.* (with callbacks). Firefox also supports chrome.* for compatibility.
  3. Container API: Firefox has a contextualIdentities API for Multi-Account Containers — unavailable in Chrome.
  4. User script API: Firefox MV3 allows userScripts API (not in Chrome MV3 currently).
  5. AMO review: Firefox add-ons require code review at addons.mozilla.org — no obfuscation allowed.

Setting Up Your First Extension

Minimum structure:

my-extension/
├── manifest.json
├── background.js (or background/service_worker.js for MV3)
└── icons/
    └── icon-48.png
Enter fullscreen mode Exit fullscreen mode

manifest.json (MV2 — still recommended for Firefox):

{
  "manifest_version": 2,
  "name": "My Extension",
  "version": "1.0",
  "description": "What it does",
  "icons": { "48": "icons/icon-48.png" },
  "permissions": ["storage", "tabs"],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": { "48": "icons/icon-48.png" }
  }
}
Enter fullscreen mode Exit fullscreen mode

For Firefox MV3 (optional, future-proof):

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup.html"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: In MV3, browser_action becomes action, and background pages become service workers.

Loading in Firefox for Development

Without signing (developer mode):

  1. Open about:debugging in Firefox
  2. Click "This Firefox" in left sidebar
  3. Click "Load Temporary Add-on"
  4. Select your manifest.json

The extension loads until Firefox closes. Good for development.

With web-ext (Mozilla's development tool):

npm install -g web-ext

# Run Firefox with your extension loaded (auto-reloads on file changes)
web-ext run --source-dir ./my-extension

# Build a distributable .xpi file
web-ext build

# Sign for distribution (needs AMO API key)
web-ext sign --api-key=$AMO_JWT_ISSUER --api-secret=$AMO_JWT_SECRET
Enter fullscreen mode Exit fullscreen mode

web-ext run is the development workflow — it reloads the extension automatically when files change.

The browser.* vs chrome.* Namespace

Firefox supports both:

// Firefox-native (with Promises):
browser.tabs.query({ active: true, currentWindow: true })
  .then(tabs => console.log(tabs[0].url));

// Chrome-compatible (with callbacks):
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
  console.log(tabs[0].url);
});
Enter fullscreen mode Exit fullscreen mode

Best practice for cross-browser extensions: Use the webextension-polyfill library:

npm install webextension-polyfill
Enter fullscreen mode Exit fullscreen mode
import browser from 'webextension-polyfill';
// Now browser.* works with Promises in both Firefox and Chrome
Enter fullscreen mode Exit fullscreen mode

Firefox-Only APIs

contextualIdentities (Multi-Account Containers):

// Create a new container
const container = await browser.contextualIdentities.create({
  name: "Work",
  color: "blue",
  icon: "briefcase"
});

// Open tab in specific container
const tab = await browser.tabs.create({
  url: "https://example.com",
  cookieStoreId: container.cookieStoreId
});
Enter fullscreen mode Exit fullscreen mode

userScripts API (MV3):

// Register a user script to run on matching pages
await browser.userScripts.register({
  js: [{ file: "content-script.js" }],
  matches: ["*://*.example.com/*"]
});
Enter fullscreen mode Exit fullscreen mode

These APIs don't exist in Chrome, so they're Firefox-only features.

Storage: Persistent Data

Extensions have access to several storage types:

// Local storage (persists until extension removed or cleared)
await browser.storage.local.set({ key: "value" });
const data = await browser.storage.local.get("key");

// Sync storage (synced via Firefox Sync if user is logged in)
await browser.storage.sync.set({ settings: { theme: "dark" } });

// Session storage (cleared when browser closes) — MV3 only
await browser.storage.session.set({ tempData: "..." });
Enter fullscreen mode Exit fullscreen mode

Capacity limits:

  • local: 5MB by default (can request unlimitedStorage permission for more)
  • sync: 100KB total, 8KB per item

Content Scripts

Content scripts run in the context of web pages:

// manifest.json
"content_scripts": [{
  "matches": ["*://*.example.com/*"],
  "js": ["content.js"],
  "css": ["styles.css"],
  "run_at": "document_end"
}]
Enter fullscreen mode Exit fullscreen mode
// content.js (runs on example.com pages)
document.querySelector('button').textContent = 'Modified by extension';

// Send message to background script
browser.runtime.sendMessage({ type: 'page-loaded', url: location.href });
Enter fullscreen mode Exit fullscreen mode

Important: Content scripts are isolated from the page's JavaScript. They share the DOM but not variables.

Messaging Between Components

// content.js → background.js
browser.runtime.sendMessage({ type: 'getData', id: 123 })
  .then(response => console.log(response));

// background.js — receive from content scripts
browser.runtime.onMessage.addListener((message, sender) => {
  if (message.type === 'getData') {
    return Promise.resolve({ data: fetchSomething(message.id) });
  }
});

// popup.js → specific tab
browser.tabs.sendMessage(tabId, { type: 'updateUI' });
Enter fullscreen mode Exit fullscreen mode

AMO Submission: What Firefox Requires

Mandatory for public listing on AMO:

  1. No obfuscated code. Minification is OK, but deliberately obfuscated code is rejected.
  2. Source code upload if using build tools (webpack, rollup) — you submit both the built .xpi and a zip of source + build instructions
  3. Privacy policy required if collecting any data
  4. Reviewer notes explaining anything non-obvious

Self-distribution: You can sign an extension for self-distribution (unlisted on AMO) if you want to distribute it yourself. Web-ext sign with --channel=unlisted creates a signed .xpi.

Why AMO review matters: AMO-reviewed extensions can't run obfuscated code that could hide malicious behavior. This is why Firefox AMO extensions are generally more trustworthy than Chrome Web Store.

Debugging Extensions

DevTools for extension background script:

  1. about:debugging → This Firefox → your extension → "Inspect"
  2. Opens a DevTools window connected to the background script
  3. Console, breakpoints, network tab all work

DevTools for popup:

  1. Open the popup (click extension icon)
  2. Right-click popup content → "Inspect Element"
  3. DevTools opens for the popup DOM

Content script debugging:
Standard DevTools on the page → Sources tab → content scripts appear under the extension's name.

Publishing

  1. Create account at addons.mozilla.org (FxA)
  2. Developer Hub → Submit a New Add-on
  3. Upload .xpi built by web-ext build
  4. Complete listing (screenshots, description, category)
  5. Submit for review

Review times vary: 1-7 days for new submissions, faster for updates. Reviewer feedback is via email.

The review process is rigorous but the reviewers are generally helpful in explaining any issues.

Top comments (0)