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:
- Go to
about:debugging#/runtime/this-firefox - Click "Load Temporary Add-on"
- 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:
- Create a developer account at addons.mozilla.org/developers
- Submit your extension ZIP (the extension folder compressed)
- Source code review: AMO reviews extensions for security and policy compliance. Extensions with minified or obfuscated code must include the original source for review.
- 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.