Store Submission, Permissions & Compliance

Submit MV3 extensions to Chrome Web Store, Firefox AMO and Safari, justify permissions under least-privilege, meet privacy disclosure requirements and avoid common rejection causes.

Getting a working extension past the store review process is where many developers stall. The code compiles, the manifest validates locally, and the extension does exactly what you intended — yet the submission bounces back with a vague rejection notice about “excessive permissions” or a missing privacy policy. This guide, part of the Manifest V3 Architecture & Extension Lifecycle section, walks through the full submission pipeline for Chrome Web Store, Firefox AMO, and the Safari Extension Gallery: how to structure your package, how to justify every permission the right way, how to meet data-disclosure requirements, and how to avoid the most common rejection causes before you hit submit.

Extension store submission and review flowDeveloper packages a zip and submits to Chrome Web Store, Firefox AMO, or Safari App Store. Each store runs automated review then human review, leading to an Approved or Rejected outcome. Approved extensions are published; rejected ones return to the developer for fixes.Developerwrites & testsPackage zipweb-ext / zipChrome WebStoreFirefox AMOaddons.mozilla.orgSafari AppStore / XcodeAutomatedreviewHumanreviewPublishedRejectedfix & resubmit

Prerequisites checklist

  • Clean, reproducible build output — no source maps, no unminified dev bundles, no .DS_Store or editor config files in the zip.
  • manifest.json passes web-ext lint (Firefox) and Chrome’s built-in manifest validator without warnings.
  • Every permission listed in "permissions" and "host_permissions" is actually used by the shipped code — unused entries are a fast-path to rejection.
  • A hosted, stable privacy-policy URL you control; stores require HTTPS and a human-readable policy, not a placeholder.
  • Justification text written for each sensitive permission (camera, microphone, tabs, broad host patterns, history, bookmarks).
  • Optional-only flows wired to chrome.permissions.request() rather than declared in the static "permissions" array.
  • Version field incremented correctly: semantic MAJOR.MINOR.PATCH.BUILD with no leading zeros.

1. Packaging the extension

Before you can submit anywhere, the extension must be zipped correctly. The zip root must contain manifest.json — not a subdirectory wrapping it.

 1// manifest.json — minimum required fields for all three stores
 2{
 3  "manifest_version": 3,
 4  "name": "My Extension",
 5  "version": "1.2.0",
 6  "description": "One concise sentence. Used verbatim in store listings.",
 7  "icons": {
 8    "16": "icons/icon-16.png",
 9    "48": "icons/icon-48.png",
10    "128": "icons/icon-128.png"
11  },
12  "permissions": ["storage"],
13  "host_permissions": ["https://api.example.com/*"],
14  "action": {
15    "default_popup": "popup.html",
16    "default_icon": "icons/icon-48.png"
17  }
18}

Execution context: Parsed by the browser extension host at install time. The "description" field is read by both the browser and each store’s review system; keep it factual and specific to your extension’s single purpose.

For Firefox, use the web-ext CLI to produce a validated package:

1# Install once
2npm install --global web-ext
3
4# Lint before packaging
5web-ext lint --source-dir ./dist
6
7# Build the zip (output goes to ./web-ext-artifacts/)
8web-ext build --source-dir ./dist --artifacts-dir ./web-ext-artifacts

Execution context: Runs in your local shell or CI environment. web-ext lint catches AMO-specific issues including missing browser_specific_settings.gecko.id, which Firefox requires for self-hosted or signed extensions. Chrome and Safari do not need an equivalent; they infer identity from the upload.

For Safari, you convert the extension with Xcode’s safari-web-extension-converter tool before any zip is involved:

1# Requires macOS + Xcode 12+
2xcrun safari-web-extension-converter ./dist \
3  --project-location ./SafariExtension \
4  --app-name "My Extension" \
5  --bundle-identifier com.example.myextension

Execution context: Runs on macOS in a local terminal. The converter wraps the extension in a native macOS/iOS app that you then build in Xcode and submit to App Store Connect. You will need an Apple Developer Program membership and must pass the App Store entitlements review.

2. Declaring permissions under least-privilege

Chrome Web Store reviewers are trained to flag any permission that seems broader than the stated purpose of the extension. The guiding rule is simple: only declare a permission if removing it would break a core user-facing feature.

MV3 introduced a cleaner permission model than MV2. There are now three distinct slots:

  • "permissions" — API permissions ("storage", "tabs", "alarms", "scripting", etc.) required at install time.
  • "host_permissions" — URL patterns the extension can access without user interaction. Reviewed more strictly than in MV2; broad patterns like "<all_urls>" or "*://*/*" require explicit justification in the submission form.
  • "optional_permissions" and "optional_host_permissions" — permissions you request at runtime, only when the user triggers a flow that needs them. These are not shown in the install dialog and do not count against your base footprint.

The correct pattern for a non-core permission is to move it to optional_permissions and request it at the point of use. The full mechanics of this flow — including the chrome.permissions.request() call, the user-gesture requirement, and how to check for permission before acting — are covered in depth in Requesting optional permissions at runtime.

 1{
 2  "manifest_version": 3,
 3  "permissions": [
 4    "storage",      // always needed: settings persistence
 5    "scripting"     // always needed: content script injection
 6  ],
 7  "host_permissions": [
 8    "https://api.example.com/*"   // specific origin, not <all_urls>
 9  ],
10  "optional_permissions": [
11    "bookmarks",    // only needed if user enables bookmark sync feature
12    "history"       // only needed if user enables history analysis feature
13  ],
14  "optional_host_permissions": [
15    "*://*.third-party-service.com/*"  // only if user connects this integration
16  ]
17}

Execution context: Parsed at install time by the extension host. optional_permissions and optional_host_permissions are never shown in the install prompt; they appear only when the extension calls chrome.permissions.request() and the user accepts the in-context dialog.

When you do need a broad host pattern, be ready to justify it in writing. The CWS submission form has a “Permission justification” field where you explain — in plain language — exactly why each sensitive permission is necessary. A one-sentence justification like “The extension needs <all_tabs> to display the word-count overlay on any page the user visits” is far more persuasive than leaving the field blank.

3. Privacy policy and data disclosure

All three stores require a privacy policy URL before your extension can go live. The policy must:

  • Be served over HTTPS at a stable URL you control.
  • Explain what user data the extension collects, how it is used, and whether it is shared with third parties.
  • Be written in plain language — legalese that obscures data practices is itself a rejection risk.

Chrome Web Store additionally requires you to complete a data use disclosure form in the developer dashboard. You must categorize every type of user data the extension handles (personal information, authentication info, financial info, health info, website content, etc.) and declare whether it is sold, used for advertising, or shared with third parties. Inaccurate disclosures — even inadvertent ones — can result in removal after approval.

Firefox AMO requires a privacy policy URL in the submission metadata and will flag an extension for human review if it collects any user data without a linked policy. AMO also has a source code submission requirement: if your code is minified, bundled, or otherwise obfuscated, you must upload a readable copy of the source (as a separate zip) alongside the built artifact. Reviewers use it to verify that the shipped code does what the listing claims. This is one of the most commonly overlooked AMO requirements.

Safari follows the App Store’s general data-collection disclosure via App Store Connect’s “App Privacy” section. You declare each data type collected, the purpose, and whether it is linked to the user’s identity. Apple’s review team audits these declarations against the extension’s actual network activity.

4. Review timelines

Plan your release schedule around realistic review windows:

Chrome Web Store runs automated scanning immediately on upload. For new publishers, human review is common and can take anywhere from one to three business days for straightforward extensions, up to several weeks if the extension touches sensitive APIs or requests broad host permissions. Established publishers with a good history tend to see shorter turnarounds. The Chrome Web Store review process detail explains how to prepare a submission that clears the automated phase cleanly and gives human reviewers exactly what they need.

Firefox AMO relies heavily on volunteer reviewers in addition to automated linting. Timelines are variable — a simple extension with no source-code submission may clear in a day; a complex extension requiring source review can take a week or more. AMO offers two tracks: “Listed” (full review, publicly searchable) and “Unlisted” (self-distribution, lighter-touch review but not discoverable in the store). Choose “Listed” unless you have a specific reason for self-distribution.

Safari Extension Gallery / App Store mirrors the standard App Store review process: typically one to three business days. Apple’s reviewers test the extension on a real device and may reject for reasons that have no Chrome or Firefox analogue — for example, an extension that does nothing visually distinctive from a built-in Safari feature, or one that does not pass through its native app wrapper cleanly.

5. Common rejection causes and how to avoid them

Understanding why submissions fail is the fastest path to getting through on the first attempt.

Excessive permissions is the leading cause of CWS rejection. If your extension declares "tabs", "history", "bookmarks", "<all_urls>", or remote code execution capabilities, each one needs a documented justification. Audit your code before submitting: search for every chrome.* API call you actually make, derive the minimal permission set from that list, and move everything else to optional_permissions.

Remote code execution is a hard block in MV3 across all three stores. You cannot fetch JavaScript from a remote server and execute it via eval(), new Function(), or document.createElement('script') with a remote src. This includes dynamically assembled content-security-policy bypasses. The CSP for MV3 extensions forbids unsafe-eval and unsafe-inline in the extension’s own pages, and stores enforce it at review time.

Unclear purpose or deceptive behavior covers a wide range of issues: extensions that claim one function in the description but request permissions for an unrelated one, hidden affiliate-link injection, silent data exfiltration, or UI that mimics a browser dialog to trick users. Write your listing description and screenshots to accurately represent every major feature.

Missing or non-compliant privacy policy blocks publication at the final step after an otherwise passing review. Set up your policy URL early in development, link it in the developer dashboard, and make sure the URL resolves correctly before you submit.

Versioning errors — duplicate version strings, versions lower than the currently published one, or non-numeric segments — cause immediate automated rejection before a human ever sees the submission.

MV3 Constraints box

  • No remote code execution: eval(), new Function(), and dynamic <script src="…"> pointing at remote URLs are prohibited. All logic must be bundled with the extension.
  • No unsafe-eval in CSP: MV3 extensions use a strict Content-Security-Policy by default. You cannot relax it to allow eval, and stores will reject manifests that attempt to.
  • host_permissions reviewed more strictly than in MV2: broad patterns (<all_urls>, *://*/*) require written justification and are increasingly subject to mandatory user-gesture restriction.
  • optional_host_permissions is MV3-only: there is no equivalent in MV2; if you support both manifest versions in the same codebase, gate optional host permission logic on the manifest version.
  • Source maps increase zip size scrutiny: include them in a development build, not in the submission zip. AMO source code uploads should be the human-readable source, not the built artifact with maps.
  • No background pages: if you relied on a persistent background page in MV2 for long-running tasks, you must redesign around the service worker eviction model before submitting an MV3 extension. See Service Worker Fundamentals for the correct patterns.

Cross-browser notes

TopicChrome Web StoreFirefox AMOSafari App Store
Review typeAutomated + human (Google staff)Automated + volunteer humanApple staff
Typical timeline (new publisher)1–3 business days, up to weeksDays to a week+1–3 business days
Source code requirementNoYes, for minified/obfuscated codeNo (but Xcode project required)
Privacy disclosure mechanismCWS data use disclosure formPrivacy policy URLApp Store Connect “App Privacy”
Broad host permission handlingJustification text required; CWS may show warning to usersFlagged for human reviewApp review tests network activity
Extension identityGoogle account + one-time $5 developer feeFirefox account, freeApple Developer Program ($99/yr)
optional_host_permissions supportFull supportFull support (Firefox 128+)Full support (Safari 17+)
Obfuscation policyProhibitedSource upload requiredProhibited

Firefox 128 shipped full optional_host_permissions support; earlier versions fall back to treating optional host permissions as required ones declared in the manifest. Test against the minimum Firefox version you declare in browser_specific_settings.gecko.strict_min_version.

Safari’s Xcode wrapper requirement means you cannot use web-ext build as your final artifact — the .app bundle produced by Xcode is what goes to App Store Connect. Factor Xcode build time into your CI pipeline if you ship cross-browser from the same source tree.