One shared source in extension/ ships to every major browser. Chromium browsers
(Chrome, Edge, Brave, Opera, Vivaldi, Arc) run the same package; Firefox gets a
transformed manifest generated by the build. Goal on each store: publish so the
vendor signs it (no install warnings) and the app can wire it up by ID.
| Browser | Store | Work needed |
|---|---|---|
| Chrome | Chrome Web Store | Upload vitreous-extension-<v>.zip |
| Brave | — | Nothing: installs from the Chrome Web Store listing |
| Opera / Vivaldi / Arc | — | Same — install from the Chrome listing |
| Edge | Microsoft Edge Add-ons | Upload the same Chrome zip |
| Firefox | addons.mozilla.org (AMO) | Upload vitreous-extension-firefox-<v>.zip |
| Safari | App Store | Not yet — needs a Mac + Xcode wrapper |
From the repo root:
npm run pack:ext # Chrome/Edge/Brave -> dist/vitreous-extension-<v>.zip
npm run pack:ext:firefox # Firefox -> dist/vitreous-extension-firefox-<v>.zip
npm run pack:ext:all # both at onceEach produces a zip with manifest.json at the zip root (required by the stores).
The Firefox build differs only in its generated manifest (background.scripts
instead of a service worker, plus a browser_specific_settings.gecko.id); the
JavaScript is identical and already cross-browser (it resolves browser on Firefox
and chrome everywhere else).
- Go to the Chrome Web Store Developer Dashboard (https://chrome.google.com/webstore/devconsole) and sign in with your existing developer account.
- Add new item → upload
vitreous-extension-<version>.zip. - Fill the store listing:
- Name: Vitreous Focus
- Summary: Blocks distracting websites behind a focus page. Companion to the Vitreous focus app.
- Category: Productivity
- Icon:
extension/icons/icon128.png - Screenshots: at least one 1280×800 or 640×400 image (a shot of the block page works).
- Privacy → Permission justifications (paste these; they speed up review):
webNavigation+tabs— detect when the user navigates to a blocked site and redirect that tab to the local block page.host_permissions: <all_urls>— a block can apply to any site the user adds, so navigations on any host must be checkable.storage— remember the on/off state and the blocked-domain list locally.- Data use: collects no user data; sends nothing to any remote server. The
extension talks only to the local Vitreous app over
127.0.0.1.
- Visibility: Unlisted.
- Submit for review.
Copy the Item ID (the 32-character extension ID shown in the dashboard) and
put it in src/main/extension-config.js as WEBSTORE_EXTENSION_ID. That ID drives
force-install for Chrome and Brave (both fetch from the Chrome Web Store).
Edge is separate — it needs its own Edge Add-ons ID (see the Edge section below).
These are Chromium browsers that install directly from the Chrome Web Store. Once the Chrome listing is live, they all work — no separate build, store, or upload. The download page can simply say "works in Brave, Opera, Vivaldi, and other Chromium browsers."
Edge runs the Chrome MV3 build unchanged, but you must publish it to the Microsoft Edge Add-ons store and give the app Edge's own item ID.
Why Edge can't reuse the Chrome listing for force-install. On an unmanaged Windows device (not domain- / Azure-AD-joined, not Chrome-Enterprise-enrolled — i.e. every normal user's PC), Edge refuses to force-install a Chrome Web Store extension. Edge's
ExtensionInstallForcelistonly silently installs items hosted in the Microsoft Edge Add-ons store. Pointing it at the CWS update URL writes the policy but Edge ignores it ("This device isn't managed by your organization…"). Chrome and Firefox honor force-install on unmanaged devices; Edge does not. So Edge needs a distinct Edge Add-ons ID + update URL. (Verified live on Edge 149, 2026-06-14.)
- Register once at the Microsoft Partner Center → Edge program (https://partner.microsoft.com/dashboard/microsoftedge) — free.
- Create new extension → upload the same
vitreous-extension-<version>.zip. - Reuse the Chrome listing copy, screenshots, and the permission justifications from §2. Set visibility to Unlisted (Edge calls it "Hidden" / not discoverable).
- Submit for review. Edge review is separate from Chrome's and usually faster.
- After approval: copy Edge's 32-char Extension ID (from the Partner Center
overview, not the Chrome ID — they differ) and put it in
src/main/extension-config.jsasEDGE_EXTENSION_ID. That's the only code change: the app already pairs it with the Edge Add-ons update URL (EDGE_UPDATE_URL) and force-installs Edge in the same single-UAC run as Chrome/Brave/Firefox. WhileEDGE_EXTENSION_IDis empty, the app deliberately leaves Edge out (so it never writes a policy Edge would ignore) and the "Add to my browser" card simply doesn't offer Edge.
- Build the Firefox package:
npm run pack:ext:firefox. - Create a free developer account at https://addons.mozilla.org and
Submit a New Add-on → upload
vitreous-extension-firefox-<version>.zip. - Choose "On your own" (self / unlisted) if you want it installable by link rather than publicly listed; Mozilla still signs it so it installs without warnings. Choose "On this site" to list it publicly in AMO search.
- Reuse the same listing copy and the permission justifications from §2.
- AMO signs the package; download the signed
.xpiit returns.
We self-distribute the signed .xpi — there is no public AMO page. The app
force-installs it itself:
- Drop the signed
.xpiatextension/signed/vitreous-firefox.xpi(version-less name — an extension update is a drop-in file replace, no code change). - The "Add to my browser" button writes the Mozilla policy
HKLM\SOFTWARE\Policies\Mozilla\Firefox\ExtensionSettings\<gecko-id>withinstallation_mode=force_installed+install_url= afile://URL to that bundled.xpi. One UAC covers Chromium browsers + Firefox together. Seesrc/main/forceinstall.js(firefox* helpers) andextension-config.js(FIREFOX_EXTENSION_ID,FIREFOX_XPI_FILENAME). - Packaging caveat: when electron-builder is added,
extension/signed/must beasarUnpack-ed so Firefox can read the.xpifrom a realfile://path (Firefox can't install from inside anapp.asararchive). - Restart required (all browsers): a force-installed extension only loads on the
browser's next launch — enterprise policies are read at startup, so it never
appears in an already-open window. The "Add to my browser" result message tells the
user to fully close (every window) and reopen the browser. There's no runtime
reload of
ExtensionSettings; the app deliberately does NOT kill/relaunch the browser (that would drop the user's tabs).
Firefox specifics (live-verified working):
- CSP must NOT upgrade
ws://→wss://. Firefox's default MV3 CSP includesupgrade-insecure-requests, which silently rewrites the bridge'sws://127.0.0.1:8736towss://and the connection then hangs forever (no error, no packet). The manifest overridescontent_security_policy.extension_pagesto drop that directive and allowws://127.0.0.1:8736inconnect-src. Do not remove this — it's the single thing that makes the Firefox bridge connect. (Chrome doesn't upgradews://localhost, so it never needed it.) - The background uses an ES-module background script (
type: "module"), supported on Firefox 128+ — hencestrict_min_version: "128.0". - Temporary add-on (
web-ext) testing caveat: host permission isn't granted by the (absent) install prompt and resets on reload, so localweb-exttesting is flaky. A real AMO-signed install grants<all_urls>at the install prompt, like Chrome's force-install — that's the reliable way to validate. - For local testing without signing:
about:debugging→ This Firefox → Load Temporary Add-on → pick the builtmanifest.json. Or usenpx web-ext runfrom the staged Firefox folder for live reload.
- A privacy-policy URL may be required (Chrome and Firefox both ask because of
host_permissions). A short page on the project's GitHub Pages site stating "no data is collected, nothing leaves your machine" satisfies all three stores. - The local load-unpacked copy keeps working for Chromium development; use
about:debuggingfor Firefox development.