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.
Top comments (0)