Skip to main content
Technology & EngineeringBrowser Extension274 lines

Extension Publishing

Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Extension 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:

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_version must be 3
  • version must follow semver-like format (e.g., 1.0.0)
  • description in manifest must not exceed 132 characters
  • Declared permissions must match what the extension actually uses

Firefox Add-ons (AMO) Requirements

Developer account:

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.id in 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 sign for 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.json and package.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> or tabs when the extension only needs activeTab triggers reviewer scrutiny, scares users at install time, and may result in rejection.

  • Skipping version increments — forgetting to bump the version in manifest.json before 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 version field in manifest.json before 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

Get CLI access →