browser_specific_settings for Firefox and Safari

Configure browser_specific_settings in an MV3 manifest: Firefox gecko id and version bounds, data_collection_permissions, gecko_android, Safari minimums, and keeping one manifest valid for every store.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

The Chrome build uploads cleanly. The same package uploaded to addons.mozilla.org fails validation with “The extension ID is required in Manifest Version 3 and above”, and once that is fixed, a later upload is rejected because the id changed. Firefox and Safari read a manifest key Chrome ignores — browser_specific_settings — and Firefox in particular relies on it for the extension’s identity, its supported version range, and newer data-collection declarations. This guide covers what belongs there and how to keep one manifest source valid everywhere. It belongs to the manifest keys reference.

What the key carries and who reads it

browser_specific_settings is an object with one sub-object per browser family. gecko is read by Firefox on desktop and holds the add-on id, minimum and maximum supported versions, an optional self-hosting update_url, and — in recent Firefox — data_collection_permissions. gecko_android overrides version bounds for Firefox for Android. safari holds Safari’s minimum and maximum versions. Chrome and Edge ignore the whole key with at most a warning on chrome://extensions, so a single manifest can carry all of it. The id is the field that matters most: Firefox uses it to sign the add-on, to match updates to the existing listing, to route native messaging, and as the stable identifier that browser.runtime.id returns.

Inside browser_specific_settingsThe gecko block with id, version bounds and data collection declarations; the gecko_android block with Android version bounds; and the safari block with version bounds.gecko.id"readable@acme.example"required for MV3 signinggecko.strict_min_version"121.0"AMO compatibilitygecko.data_collection_permissionsrequired / optionalnewer Firefoxgecko_androidAndroid version boundsoptionalsafari.strict_min_version"16.4"Safari bound
Chrome ignores the whole key, so one manifest can serve every store.

Step-by-step: a complete, portable block

1. Choose a permanent Firefox id

1"browser_specific_settings": {
2  "gecko": { "id": "readable@acme.example" }
3}

Execution context: the manifest, read by Firefox at install and by AMO at upload. The id can be an email-like string (name@domain) or a GUID in braces ({9c1f…}); the email form is easier to read in logs and native host manifests. Choose it before the first upload and never change it — a new id is a new add-on, and existing users will not receive updates. The email form does not need to be a real address, but using a domain you own avoids collisions.

2. Set version bounds

1"gecko": {
2  "id": "readable@acme.example",
3  "strict_min_version": "121.0"
4},
5"gecko_android": {
6  "strict_min_version": "121.0"
7}

Execution context: the manifest. strict_min_version must use dotted form. Omit strict_max_version unless you know a future release breaks you; with it set, AMO marks the add-on incompatible with newer Firefox until you upload again. gecko_android overrides the bounds for Firefox for Android — include it only if you support Android, where several APIs (native messaging, some windows features) are unavailable. Picking the minimum is covered in setting minimum browser versions.

Who reads which fieldWhich browsers read each part of browser_specific_settings and what happens when it is missing.FieldChrome / EdgeFirefoxSafarigecko.idIgnoredRequired (MV3)Ignoredgecko.strict_min_versionIgnoredEnforced on AMOIgnoredgecko.data_collection_permissionsIgnoredInstall prompt (newer)Ignoredsafari.strict_min_versionIgnoredIgnoredReadBlock missing entirelyFineUpload rejectedFine
Only Firefox fails hard when the block is missing; the others merely ignore it.

3. Declare data collection for Firefox

Recent Firefox versions show what data an add-on collects in the install prompt, driven by a manifest declaration rather than only by the listing.

1"gecko": {
2  "id": "readable@acme.example",
3  "strict_min_version": "121.0",
4  "data_collection_permissions": {
5    "required": ["none"],
6    "optional": ["technicalAndInteraction"]
7  }
8}

Execution context: the manifest. "none" states that the add-on collects nothing by default; optional categories can be requested at runtime the same way optional permissions are. Check Mozilla’s current documentation for the category names and the Firefox versions that enforce the declaration — AMO began requiring it for new submissions and the list of categories has evolved. Whatever you declare must match what the code does and what the privacy policy says.

4. Add Safari bounds if you distribute there

1"safari": { "strict_min_version": "16.4" }

Execution context: the manifest packaged inside the Safari app. In practice the containing app’s deployment target in Xcode is the stronger constraint — an app built for macOS 13 cannot run on 12 regardless of the manifest — but declaring the Safari version documents which Safari features (for example storage.session, available from 16.4) the extension relies on.

One manifest source, three store packagesA shared manifest template with browser_specific_settings is transformed per target: Chrome drops nothing but ignores the block, Firefox swaps the background form, Safari is wrapped in an Xcode app.manifest.base.jsonincl. browser_specific_settingsbuild --targetchrome · firefox · safarivalidateweb-ext lint, schemaper-target packagesChrome zipblock ignoredFirefox xpibackground.scriptsSafari appXcode project
Keep the block in the shared template; vary only what each store truly needs differently.

5. Validate before uploading

1npx web-ext lint --source-dir dist/firefox

Execution context: a terminal or CI step on the Firefox build. web-ext lint runs the same validator AMO uses and catches a missing id, a malformed version, unknown permissions and unsupported keys before an upload round-trip. It also flags Chrome-only keys as warnings, which is your cue to strip them from the Firefox build or accept them as harmless.

6. Keep the legacy name out of new manifests

Older Firefox manifests used "applications" instead of "browser_specific_settings". MV3 accepts only the new name.

1- "applications": { "gecko": { "id": "readable@acme.example" } }
2+ "browser_specific_settings": { "gecko": { "id": "readable@acme.example" } }

Execution context: the manifest. AMO rejects applications in MV3 packages. If you are migrating from MV2, this rename belongs in the same change as manifest_version: 3.

7. Self-hosted Firefox builds

Add-ons distributed outside AMO — enterprise builds, beta channels — can point Firefox at your own update manifest with gecko.update_url. The package must still be signed by AMO through the unlisted channel; the update URL only tells Firefox where to look for new versions.

1"gecko": {
2  "id": "readable-beta@acme.example",
3  "update_url": "https://updates.acme.example/firefox/updates.json"
4}

Execution context: the manifest of a self-distributed Firefox build. Use a different gecko.id for the beta channel so it can be installed alongside the listed release. The updates.json file lists versions and signed .xpi URLs per add-on id; Firefox polls it roughly daily. AMO-listed builds must not include update_url — AMO handles updates for them and rejects packages that try to override it.

Common mistakes

  • Changing the gecko id. The id is the add-on’s identity on AMO; changing it creates a new add-on and strands existing users on the old one.
  • Using a short version string. "121" fails AMO validation; use "121.0".
  • Setting strict_max_version “to be safe”. It blocks your add-on on every future Firefox until you re-upload.
  • Assuming Chrome’s key gives Firefox a stable id. It does not; only gecko.id does.
  • Declaring data collection that the code contradicts. Reviewers compare the declaration, the privacy policy and the code. A mismatch is grounds for rejection.

Cross-browser variation

  • Chrome / Edge: ignore browser_specific_settings. Edge Add-ons does not use it either.
  • Firefox: requires gecko.id for MV3; reads version bounds and, in recent versions, data collection declarations. Firefox for Android reads gecko_android overrides.
  • Safari: reads the safari bounds; identity comes from the containing app’s bundle identifier.

Verification

  1. Run npx web-ext lint on the Firefox build: no errors.
  2. Load the build in Firefox via about:debugging → “This Firefox” → “Load Temporary Add-on” and confirm the displayed id matches gecko.id.
  3. Load the same manifest.json in Chrome and confirm only a warning (or nothing) is shown for the Firefox block.
  4. Run browser.runtime.id in the Firefox background console and confirm it returns the gecko id.

FAQ

Do I need gecko.id for a temporary add-on during development?

Not strictly — Firefox assigns a temporary id — but without a fixed id, storage and native messaging change every load. Set it from the start.

Can I use the same id format for Chrome?

No. Chrome ids are derived from a public key and are always 32 letters from a to p. The two identities are independent.

Where does Safari get its extension identifier?

From the bundle identifier of the extension target in the Xcode project, typically com.acme.Readable.Extension.

Does Firefox need anything else Chrome does not?

For MV3, the main differences live outside this key: Firefox prefers background.scripts over background.service_worker, uses sidebar_action instead of side_panel, and supports theme_icons for the action. Generate those per target rather than carrying them all in one manifest, so each store sees a clean file without warnings it has to wave through.

Should the Firefox id match anything in Chrome?

No. Keep the identities independent and record both in one place — a small ids.json in the repository works — so native host manifests, server allow-lists and documentation can be generated from a single source.

Other MV3 Architecture & Extension Lifecycle Resources