Publishing Private and Enterprise Extensions

Distribute an MV3 extension to a company or a closed group — unlisted and private store listings, force-installing with ExtensionInstallForcelist, self-hosting updates, and policy-configured settings.

Published September 18, 2026 Updated September 18, 2026 7 min read
Table of Contents

Not every extension should be public. An internal tool, a pilot for one customer, or a build for a regulated environment needs to reach a known set of machines, update reliably, and ideally be configured centrally rather than by each user. The stores and the browsers all support this — through different mechanisms that are easy to confuse, and with one constraint that surprises people: self-hosting outside the store is far more restricted on Windows and macOS than it used to be. This guide is part of store submission and permissions compliance.

The distribution options

Ways to distribute an extension to a limited audienceUnlisted listing, private domain listing, force-install by policy from the store, and self-hosted force-install compared on who can install, review requirements and update mechanism.RouteWho can installStore reviewUpdates viaUnlisted store itemAnyone with the linkYesStorePrivate (domain) itemYour Workspace domainYesStoreStore item, force-installedManaged devicesYesStoreSelf-hosted, force-installedManaged devices onlyNoYour update URL
Every route except self-hosting still goes through store review — the audience is restricted, the policy is not.

Step-by-step

An unlisted item is published normally but excluded from search and category pages. It is right for a beta, a partner pilot or a tool for a small community — and wrong for anything confidential, because the link can be shared.

Set visibility to Unlisted in the developer dashboard’s distribution settings. Review, policies and the update path are identical to a public item.

2. Pick a private listing for “only my organisation”

A private item is visible only to users signed in to a specific Google Workspace domain. The listing is invisible and uninstallable to everyone else.

This requires publishing from an account inside that Workspace, or one the domain has trusted, and it is the right default for internal tools that do not need to be force-installed.

3. Force-install from the store with enterprise policy

For managed machines, administrators can install an extension silently and prevent its removal. The extension is still a normal store item — public, unlisted or private — and updates through the store.

1{
2  "ExtensionInstallForcelist": [
3    "abcdefghijklmnopabcdefghijklmnop;https://clients2.google.com/service/update2/crx"
4  ]
5}

Execution context: Chrome enterprise policy, delivered by Group Policy on Windows, a configuration profile on macOS, a JSON file under /etc/opt/chrome/policies/managed/ on Linux, or the Google Admin console. The value is <extension-id>;<update-url>; the store’s update URL is the standard one shown.

4. Self-host only where the platform allows it

Self-hosting — serving your own .crx and update manifest — skips store review, and for that reason is tightly restricted. On Windows and macOS, Chrome only installs off-store extensions that are force-installed by policy on a managed machine. On Linux and ChromeOS the rules are looser but policy is still the supported path.

1<!-- https://ext.example.com/updates.xml -->
2<?xml version="1.0" encoding="UTF-8"?>
3<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
4  <app appid="abcdefghijklmnopabcdefghijklmnop">
5    <updatecheck codebase="https://ext.example.com/reader-2.4.0.crx" version="2.4.0" />
6  </app>
7</gupdate>

Execution context: a static file on your server, fetched by the browser on its update schedule. The .crx must be signed with the same private key every time — lose the key and every installed copy is stranded, because a new key means a new extension id.

1{
2  "update_url": "https://ext.example.com/updates.xml"   // in the extension's manifest
3}

Execution context: parsed at install. The store rejects packages that contain update_url, so a self-hosted build and a store build are separate artifacts — the per-target generation in generating a manifest per browser target is the natural home for this.

5. Let administrators configure it

Enterprise deployments want settings pushed centrally — a server URL, a tenant id, a feature switch. Extensions declare a schema, and administrators supply values through the same policy channel.

1{
2  "storage": { "managed_schema": "managed_schema.json" }
3}
1{
2  "type": "object",
3  "properties": {
4    "serverUrl": { "type": "string", "title": "API server" },
5    "allowPersonalSync": { "type": "boolean", "title": "Allow personal sync" }
6  }
7}

Execution context: parsed at install. Values arrive in chrome.storage.managed, which is read-only to the extension and is the only storage area an administrator can write.

1async function effectiveSettings() {
2  const [managed, user] = await Promise.all([
3    chrome.storage.managed.get(null).catch(() => ({})),   // empty when not managed
4    readSettings(),
5  ]);
6  return { ...user, ...managed, lockedKeys: Object.keys(managed) };
7}

Execution context: any extension context. Managed values override user values, and lockedKeys lets the options page disable the corresponding controls with an explanation — “Set by your organisation” — rather than letting the user change something that will immediately revert. The schema-parsing side of this is in defaulting and versioning an options schema.

Where an enterprise setting comes fromThree layers of settings resolved in priority order: administrator policy in storage.managed, the user's own choices, and code-owned defaults.storage.managedpushed by the administratorread-only, highest prioritystorage.sync / localthe user's own choicesignored for locked keysCode defaultsfrozen DEFAULTS objectused when neither sets a value
Policy wins, but the UI must say so — a control that silently reverts looks broken.

Operating a private extension over time

Private distribution changes who you answer to. A public extension has thousands of anonymous users; a private one has an IT department with a change-management process, and the practices that suit each are different.

Version pinning is normal. Many organisations want to test an update before it reaches their fleet. With store distribution you cannot pin a version for one organisation — everyone gets the published version — so communicate releases ahead of time and keep changes backwards compatible. With self-hosting, the administrator controls updates.xml and can hold a version back.

Policy is part of the contract. Once an organisation deploys a managed_schema value, renaming that key breaks their configuration silently. Treat the managed schema like a public API: add keys freely, deprecate slowly, never rename.

Signing keys are critical infrastructure. For self-hosted distribution the private key is the extension’s identity. Store it like a production secret, back it up, and restrict who can sign — the practices in managing store credentials in CI secrets apply directly.

Choosing a private distribution routeA decision tree from the audience and management situation to unlisted, private, store force-install or self-hosted distribution.Who needs the extension, and are their machines managed?Invited people, unmanagedUnlisted listinglink-onlyNot confidentiallinks get sharedOur Workspace usersPrivate listingdomain-restrictedUsers install themselvesstore updatesManaged fleetStore item + policyExtensionInstallForcelistSilent install, no removalstore updatesAir-gapped or no storeSelf-hostedyour CRX + update URLGuard the signing keyit is the extension id
Self-hosting is the last branch for a reason — it is the only one that makes you responsible for signing and update infrastructure.

Cross-browser variation

  • Chrome / Edge: unlisted and private listings, ExtensionInstallForcelist, storage.managed with a managed_schema, and self-hosting for managed machines. Edge mirrors Chrome’s policy names under its own policy path and add-ons store.
  • Firefox: enterprise policies via policies.json or Group Policy, with an Extensions / ExtensionSettings policy for force-installation. Self-distributed add-ons must still be signed by Mozilla — unlisted AMO submission provides that signature without a public listing.
  • Safari: distribution goes through the App Store or through the organisation’s device management, since the extension ships inside an app. Managed app configuration replaces storage.managed.
  • All three: force-installed extensions can be pinned and prevented from being disabled by the user. Design the UI to acknowledge that — a “disable” option the user cannot actually use is confusing.

Verification

  1. On a machine with the policy applied, open chrome://policy and confirm ExtensionInstallForcelist lists your id with status OK.
  2. Confirm managed settings arrive:
1await chrome.storage.managed.get(null);
2// { serverUrl: "https://api.corp.example", allowPersonalSync: false }

Execution context: the service worker console on a managed machine. On an unmanaged machine this resolves to an empty object — the catch in the reader is for browsers where storage.managed is absent.

  1. Confirm the options page disables locked controls and labels them.
  2. For self-hosting, bump the version in updates.xml, then use Update on chrome://extensions and confirm the managed machine picks it up.

FAQ

Can I self-host for users outside my organisation?

Not on Windows or macOS Chrome — off-store extensions only install when force-installed by policy on managed devices. Use an unlisted store listing instead.

Does a private listing still get reviewed?

Yes. Every store item is subject to the same program policies regardless of visibility. Private distribution restricts the audience, not the rules.

How do I change a managed setting’s type safely?

Add a new key with the new type, read both during a transition, and document the old one as deprecated. Administrators update policy on their own schedule, often months behind your releases.

Other MV3 Architecture & Extension Lifecycle Resources