One codebase, two browser engines
CoApp is a browser extension that surfaces operational alerts directly inside any browser-based workflow. When a patient record contains a discrepancy, when a transaction falls outside a configured threshold, or when a workflow step is skipped — CoApp intercepts that moment and flags it before it becomes a costly mistake. To do that reliably across enterprise environments, a single codebase needs to run on Chrome, Edge, and Firefox with identical behavior and no version drift.
That's a harder engineering problem than it sounds. The browser extension ecosystem is fragmented across two major engine families — Chromium (Chrome, Edge, Brave) and Gecko (Firefox) — each with its own manifest format, background page architecture, signing requirements, and update distribution model. This post walks through exactly how we handle those differences at CoApp, and the self-hosted delivery pipeline that keeps all three browsers consistently up to date.
The Chromium foundation
Chrome and Edge share the same Chromium extension platform, which means a single extension build runs on both without modification. CoApp targets Manifest V3, the current standard, which replaced persistent background pages with service workers. This is a meaningful architectural shift: where a Manifest V2 background page ran continuously, a Manifest V3 service worker is event-driven and may be terminated by the browser at any time.
For CoApp, this means all state that needs to survive across browser sessions is written to chrome.storage.local rather than held in memory. Event listeners are registered at the top level of the service worker so they're available immediately when the worker activates. Communication between the service worker and content scripts uses chrome.runtime.sendMessage with structured response handling. Content scripts inject into target pages based on URL match patterns declared in the manifest, and they request only the permissions they need — no more.
Because a service worker can be terminated mid-operation, CoApp's service worker checkpoints long-running tasks to chrome.storage.local before any async boundary. If the worker is killed and restarts, it picks up exactly where it left off.
Edge adds one wrinkle: Microsoft's enterprise policies layer additional controls over extension installation. But because Edge respects the same group policy keys as Chrome for force-installed extensions, our deployment pipeline handles both identically.
Firefox is a different world
Firefox uses the Gecko engine and the WebExtensions API, which overlaps significantly with Chrome's extension API — but not completely. The differences that matter most for CoApp are in three areas: the manifest format, the background page architecture, and the extension ID.
Firefox still supports Manifest V2 for background pages, using background.scripts rather than service_worker. This means the Firefox version of CoApp runs a persistent background page instead of a service worker. The tradeoff is simpler state management (no risk of worker termination) at the cost of higher memory overhead. For enterprise workstations running CoApp alongside practice management software, this is an acceptable tradeoff — and one we'll revisit as Firefox's MV3 support matures.
The extension ID is managed via browser_specific_settings in the manifest. This is critical for self-hosted distribution: without a stable, declared extension ID, Firefox won't match incoming updates to the installed extension. The ID is a UUID assigned when you first sign the extension through Mozilla's signing infrastructure, and it must remain constant across all future releases.
Finally, Firefox uses .xpi packaging rather than Chrome's .crx format, and Mozilla requires all extensions distributed outside the Add-ons store to be signed using the web-ext sign tool with a valid API key. Unsigned extensions will not load in standard Firefox builds.
Self-hosted distribution
Both Chrome and Firefox support self-hosted extension distribution — meaning you can bypass the web store entirely, host your own extension file and update manifest on infrastructure you control, and push updates to all installed instances on your own schedule. For enterprise software like CoApp, this is the right call. Web stores introduce non-deterministic review latency and take control of your update cadence out of your hands.
Our distribution model is straightforward: the built extension file (either .crx for Chrome or .xpi for Firefox) is uploaded to S3. A CloudFront distribution sits in front of it for low-latency delivery to enterprise workstations anywhere in the country. A separate update manifest file — a small XML document — sits at a stable URL and tells the browser what version is current and where to download it.
The build pipeline
The build process starts with web-ext build, which packages the extension source into a clean .zip archive. For Firefox, that archive goes directly to web-ext sign, which contacts Mozilla's signing API and returns a signed .xpi. For Chrome, the .crx packaging step is currently manual — a gap in our pipeline we're working to close.
Once the extension files are built and signed, the deployment sequence is:
- Upload the new extension file to S3 with a versioned key
- Update the update manifest XML to point to the new version and file URL
- Issue a CloudFront invalidation to flush the cached manifest
- Verify the update manifest is serving the correct version at the CDN edge
Enterprise workstations receive the extension via group policy. A force-install policy entry points Chrome and Edge to CoApp's update URL. When the browser next checks for extension updates — typically every few hours — it fetches the manifest, compares the declared version against the installed version, and downloads the new file if a newer version is available.
What's next
The most immediate item on the roadmap is automating the Chrome .crx packaging step to match the Firefox workflow. Right now, Firefox packaging is fully scripted end-to-end via web-ext. Chrome requires a manual step in the Chrome developer dashboard to generate the .crx file with the correct private key. We're building a CI job that handles this automatically on each tagged release, so both browser packages are produced and deployed in a single pipeline run.
Longer term, we're watching Firefox's Manifest V3 adoption closely. Mozilla has committed to MV3 support while preserving more permissive background processing than Chrome's implementation. When that story stabilizes, we'll evaluate converging on a single manifest version — which would simplify the build considerably and reduce the surface area for browser-specific bugs.
