Extension Publishing
Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation
You are an expert in publishing browser extensions to Chrome Web Store and addons.mozilla.org. ## Key Points - One-time $5 registration fee at https://chrome.google.com/webstore/devconsole - Verify your developer identity (email or website) - Extension name (max 75 characters) - Description (up to 132 characters for summary) - Detailed description - At least one screenshot (1280x800 or 640x400 recommended) - Icon: 128x128 PNG - Promotional images (optional): small tile 440x280, marquee 1400x560 - Category selection - Privacy policy URL (required if the extension handles user data) - `manifest_version` must be 3 - `version` must follow semver-like format (e.g., `1.0.0`)
skilldb get browser-extension-skills/Extension PublishingFull skill: 274 linesExtension Publishing — Browser Extension Development
You are an expert in publishing browser extensions to Chrome Web Store and addons.mozilla.org.
Overview
Publishing a browser extension involves packaging the code, preparing store listings, submitting for review, and maintaining updates. Chrome Web Store (CWS) and Firefox Add-ons (AMO) each have their own requirements, review processes, and policies. Understanding these processes is essential for successful distribution.
Core Concepts
Packaging the Extension
Extensions are packaged as ZIP files for submission:
# Create a production build
npm run build
# Package for Chrome
cd dist/chrome && zip -r ../../extension-chrome.zip . -x "*.map" "*.DS_Store"
# Package for Firefox
cd dist/firefox && zip -r ../../extension-firefox.zip . -x "*.map" "*.DS_Store"
Ensure the ZIP contains manifest.json at the root level, not inside a subdirectory.
Chrome Web Store Requirements
Developer account setup:
- One-time $5 registration fee at https://chrome.google.com/webstore/devconsole
- Verify your developer identity (email or website)
Required listing assets:
- Extension name (max 75 characters)
- Description (up to 132 characters for summary)
- Detailed description
- At least one screenshot (1280x800 or 640x400 recommended)
- Icon: 128x128 PNG
- Promotional images (optional): small tile 440x280, marquee 1400x560
- Category selection
- Privacy policy URL (required if the extension handles user data)
Manifest requirements:
manifest_versionmust be 3versionmust follow semver-like format (e.g.,1.0.0)descriptionin manifest must not exceed 132 characters- Declared permissions must match what the extension actually uses
Firefox Add-ons (AMO) Requirements
Developer account:
- Free registration at https://addons.mozilla.org/developers/
- No registration fee
Required listing assets:
- Extension name
- Summary (max 250 characters)
- Description (supports HTML formatting)
- Icon: ideally 128x128
- Screenshots (recommended)
- Categories and tags
- License selection
- Privacy policy (if applicable)
Firefox-specific requirements:
browser_specific_settings.gecko.idin manifest (email format or UUID)- Source code may be required for review if the extension uses a build/minification step
- Extensions must be signed by Mozilla (automatic on AMO, or use
web-ext signfor self-distribution)
Implementation Patterns
Automated Build and Packaging
// package.json scripts
{
"scripts": {
"build": "node build.js",
"build:chrome": "BUILD_TARGET=chrome node build.js",
"build:firefox": "BUILD_TARGET=firefox node build.js",
"package:chrome": "npm run build:chrome && cd dist/chrome && zip -r ../../release/chrome.zip .",
"package:firefox": "npm run build:firefox && cd dist/firefox && zip -r ../../release/firefox.zip .",
"package:all": "npm run package:chrome && npm run package:firefox",
"lint:firefox": "npx web-ext lint --source-dir dist/firefox"
}
}
Firefox web-ext Tool
Mozilla's official CLI for development and publishing:
npm install -g web-ext
# Run the extension in a temporary Firefox profile
web-ext run --source-dir dist/firefox
# Lint the extension for AMO compatibility
web-ext lint --source-dir dist/firefox
# Build the ZIP
web-ext build --source-dir dist/firefox --artifacts-dir release
# Sign and publish (requires API credentials)
web-ext sign \
--source-dir dist/firefox \
--api-key=$AMO_JWT_ISSUER \
--api-secret=$AMO_JWT_SECRET
Chrome Web Store API for Automated Publishing
// publish-chrome.js
// Uses the Chrome Web Store Publish API
import fetch from 'node-fetch';
const EXTENSION_ID = 'your-extension-id';
const CLIENT_ID = process.env.CWS_CLIENT_ID;
const CLIENT_SECRET = process.env.CWS_CLIENT_SECRET;
const REFRESH_TOKEN = process.env.CWS_REFRESH_TOKEN;
async function getAccessToken() {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: REFRESH_TOKEN,
grant_type: 'refresh_token'
})
});
const data = await response.json();
return data.access_token;
}
async function uploadAndPublish(zipPath) {
const token = await getAccessToken();
const zipBuffer = await import('fs').then((fs) => fs.readFileSync(zipPath));
// Upload
const uploadRes = await fetch(
`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${EXTENSION_ID}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'x-goog-api-version': '2'
},
body: zipBuffer
}
);
const uploadResult = await uploadRes.json();
console.log('Upload result:', uploadResult);
// Publish
const publishRes = await fetch(
`https://www.googleapis.com/chromewebstore/v1.1/items/${EXTENSION_ID}/publish`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'x-goog-api-version': '2'
}
}
);
const publishResult = await publishRes.json();
console.log('Publish result:', publishResult);
}
uploadAndPublish('release/chrome.zip');
CI/CD Pipeline (GitHub Actions)
# .github/workflows/publish.yml
name: Build and Publish Extension
on:
push:
tags:
- 'v*'
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run package:all
- name: Upload Chrome extension
uses: mnao305/chrome-extension-upload@v5.0.0
with:
file-path: release/chrome.zip
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
client-id: ${{ secrets.CWS_CLIENT_ID }}
client-secret: ${{ secrets.CWS_CLIENT_SECRET }}
refresh-token: ${{ secrets.CWS_REFRESH_TOKEN }}
publish: true
- name: Upload Firefox extension
run: npx web-ext sign --source-dir dist/firefox --api-key=${{ secrets.AMO_JWT_ISSUER }} --api-secret=${{ secrets.AMO_JWT_SECRET }}
Version Management
// bump-version.js — update version across manifest and package.json
import { readFileSync, writeFileSync } from 'fs';
const newVersion = process.argv[2];
if (!newVersion) {
console.error('Usage: node bump-version.js 1.2.3');
process.exit(1);
}
// Update package.json
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
pkg.version = newVersion;
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
// Update manifest
const manifest = JSON.parse(readFileSync('src/manifest.base.json', 'utf-8'));
manifest.version = newVersion;
writeFileSync('src/manifest.base.json', JSON.stringify(manifest, null, 2) + '\n');
console.log(`Version bumped to ${newVersion}`);
Best Practices
- Submit source code proactively to AMO if you use any build step (bundler, minifier, transpiler) — reviewers will request it, and providing it upfront speeds up approval.
- Write a clear and honest description of what the extension does and why each permission is needed — vague descriptions or unexplained permissions are the most common reasons for review rejection.
- Automate the build-package-publish pipeline so that releases are reproducible and version numbers stay consistent across
manifest.jsonandpackage.json.
Core Philosophy
Publishing is not the last step of development — it is a continuous process that should be as automated and repeatable as running your test suite. Every manual step in the publish pipeline is a potential source of error: a forgotten version bump, a missing screenshot, or an unsigned build. Treat your release process as code, version it, and run it in CI.
Transparency with store reviewers accelerates approval. Write honest, specific descriptions of what your extension does and why each permission is needed. Reviewers are overworked and suspicious of vague descriptions; a clear explanation like "uses tabs permission to read the current tab's URL for bookmark saving" resolves their concern in seconds. Proactively submitting source code to AMO when using a build step saves a round-trip rejection.
Think of your store listing as a product page, not a formality. Users decide to install based on the listing quality — clear screenshots, a concise description, and a privacy policy that does not read like a legal threat. Investing thirty minutes in a good listing pays off in higher install rates and fewer support requests from confused users.
Anti-Patterns
-
Publishing without a CI pipeline — manually building, zipping, and uploading extensions leads to inconsistent artifacts, forgotten steps, and unreproducible releases; automate the entire flow from build to store upload.
-
Requesting broad permissions "just in case" — asking for
<all_urls>ortabswhen the extension only needsactiveTabtriggers reviewer scrutiny, scares users at install time, and may result in rejection. -
Skipping version increments — forgetting to bump the version in
manifest.jsonbefore uploading causes the store to reject the submission silently or with a confusing error. -
Minifying without providing source — submitting minified or bundled code to AMO without accompanying source code guarantees a reviewer rejection and delays your release by days.
-
Ignoring store policy updates — Chrome Web Store and AMO regularly update their policies around remote code execution, data handling, and permission justifications; failing to track these changes leads to surprise rejections of previously approved extensions.
Common Pitfalls
- Requesting permissions the extension does not actually use — both Chrome and Firefox reviewers flag this and will reject the submission until unnecessary permissions are removed.
- Forgetting to increment the
versionfield inmanifest.jsonbefore uploading an update — the store rejects uploads where the version is the same as or lower than the currently published version.
Install this skill directly: skilldb add browser-extension-skills
Related Skills
Background Workers
Service workers and background scripts for event-driven extension logic, lifecycle management, and persistent tasks
Content Scripts
Content scripts for DOM manipulation, page interaction, and isolated world execution in browser extensions
Cross-Browser
Cross-browser extension compatibility across Chrome, Firefox, and Safari with API normalization and build strategies
Manifest V3
Chrome Manifest V3 fundamentals including configuration, permissions, and migration from V2
Messaging
Message passing between extension components including content scripts, service workers, popups, and external pages
Popup UI
Building popup and options page UIs for browser extensions with responsive layouts and framework integration