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.
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
Step-by-step
1. Pick unlisted for “anyone I send the link to”
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.
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.
Cross-browser variation
- Chrome / Edge: unlisted and private listings,
ExtensionInstallForcelist,storage.managedwith amanaged_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.jsonor Group Policy, with anExtensions/ExtensionSettingspolicy 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
- On a machine with the policy applied, open
chrome://policyand confirmExtensionInstallForcelistlists your id with status OK. - 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.
- Confirm the options page disables locked controls and labels them.
- For self-hosting, bump the version in
updates.xml, then use Update onchrome://extensionsand 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.
Related
- Passing Chrome Web Store review — the review every store route still requires.
- Publishing to Firefox add-ons and Safari — the other stores’ mechanics.
- Generating a manifest per browser target — keeping store and self-hosted builds separate.
- Store submission and permissions compliance — the parent guide.