How I Built and Published My First Firefox Extension

I built Tab Manager & Session Saver as a side project. Here's what I learned about building and shipping a Firefox extension.

The Core APIs You Need

Firefox extensions use the WebExtension API, which is largely cross-compatible with Chrome. For a tab management extension, the main APIs are:

// Query all open tabs browser.tabs.query({ currentWindow: true }); // Get all windows browser.windows.getAll({ populate: true }); // Save data locally (persistent across restarts) browser.storage.local.set({ sessions: [...] }); browser.storage.local.get('sessions'); // Open a URL in a new tab browser.tabs.create({ url: 'https://example.com' }); // Close a tab browser.tabs.remove(tabId); 

Firefox's API uses browser.* (with Promise-based async). Chrome uses chrome.* (with callbacks, though Chrome now also supports Promises in Manifest V3). For cross-browser compatibility, you can use the webextension-polyfill library.

The Manifest

The manifest.json is the entry point. For a popup extension that accesses tabs:

{ "manifest_version": 2, "name": "Tab Manager", "version": "1.0", "description": "Save and restore named tab sessions", "permissions": [ "tabs", "storage" ], "browser_action": { "default_popup": "popup.html", "default_icon": "icon.png" } } 

Note on Manifest V3: Firefox supports both MV2 and MV3. Chrome is phasing out MV2. For extensions that need tabs and storage permissions (like a session manager), MV2 is still the simpler choice if you target Firefox. For cross-browser MV3 support, the API changes are mostly about background scripts (service_worker instead of scripts).

The Popup

The popup (popup.html) is the extension's UI. It's a regular HTML page that loads when the toolbar icon is clicked. For a sessions list:

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="popup.css"> </head> <body> <div id="sessions-list"></div> <button id="save-btn">Save current tabs</button> <script src="popup.js"></script> </body> </html> 
// popup.js document.getElementById('save-btn').addEventListener('click', async () => { const tabs = await browser.tabs.query({ currentWindow: true }); const session = { name: prompt('Session name:') || 'Unnamed', tabs: tabs.map(t => ({ url: t.url, title: t.title })) }; const { sessions = [] } = await browser.storage.local.get('sessions'); sessions.push(session); await browser.storage.local.set({ sessions }); renderSessions(); }); async function renderSessions() { const { sessions = [] } = await browser.storage.local.get('sessions'); const list = document.getElementById('sessions-list'); list.innerHTML = sessions.map((s, i) => `<div>${s.name} (${s.tabs.length} tabs) <button data-i="${i}">Restore</button></div>` ).join(''); list.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', async () => { const { sessions } = await browser.storage.local.get('sessions'); const session = sessions[parseInt(btn.dataset.i)]; for (const tab of session.tabs) { await browser.tabs.create({ url: tab.url }); } }); }); } renderSessions(); 

This is the core. Real implementations add: delete/rename sessions, open in new window, error handling for invalid URLs.

Local Development

Load the extension in Firefox without packaging:

  1. Go to about:debugging#/runtime/this-firefox
  2. Click "Load Temporary Add-on"
  3. Select your manifest.json

The extension loads and runs until Firefox is closed. Changes to popup HTML/JS take effect on the next click of the toolbar icon (no reload needed). Background script changes require "Reload" in about:debugging.

Submitting to AMO

AMO (addons.mozilla.org) is Mozilla's extension store. The review process:

  1. Create a developer account at addons.mozilla.org/developers
  2. Submit your extension ZIP (the extension folder compressed)
  3. Source code review: AMO reviews extensions for security and policy compliance. Extensions with minified or obfuscated code must include the original source for review.
  4. Wait for review (currently 1-4 weeks for self-hosted; faster for AMO-hosted)

Self-hosted vs AMO-hosted: AMO-hosted extensions get updates through AMO. Self-hosted extensions manage their own updates (you provide an update URL in the manifest). AMO-hosted is simpler for most use cases.

Policies to know:

  • Extensions can't collect user data without disclosure and consent
  • Remote code execution is prohibited
  • Extensions must do what the description says

What I'd Do Differently

Use Promises everywhere from the start: I initially used callback-style code in some places, which led to inconsistent error handling. The browser.* API is fully Promise-based — use async/await throughout.

Design the storage schema first: I changed my storage format twice. Session data is simple (name + list of tabs), but planning the schema upfront avoids migrations.

Write the README before the extension: Forces you to think about what the extension actually does and why, which sharpens the implementation.

The finished extension is Tab Manager & Session Saver on AMO. Source is available if you want to look at a real implementation.