ADR-013 — Extension versioning strategy
1. Context
The Noxys browser extension currently ships with 0.1.0 hardcoded in three places: package.json, chrome/manifest.json, and firefox/manifest.json. There is no build metadata (commit SHA, build number, channel marker), no separation between development/QA builds and production releases, no telemetry to know which version a tenant is running, and no automated release pipeline. The result:
- Bug reports are ambiguous — the Console operator cannot tell which build a user has installed.
- QA cannot install staging side-by-side with stable on the same browser profile.
- We have no way to detect that a tenant is running an outdated extension that lacks security patches.
- Manual store uploads are slow, error-prone, and lack traceability to commits.
We need a versioning strategy in place before opening the Free tier signup and onboarding the first ~50 active users (W27-28 per ADR-010).
2. Decision summary
- 2 channels only:
stable+staging(no separatebetachannel — beta phase = staging with1.0.0-rc.Nversions). - 2 distinct extension IDs: stable on Chrome Web Store + Firefox AMO; staging sideloaded only and never published to public stores. Side-by-side install supported on the same browser profile.
- SemVer schema with channel-aware suffix; CI auto-bumps staging build number from
github.run_number. - Hybrid distribution for staging: GitHub Release
.zipfor first install + Cloudflare R2 self-hosted update server (update.xml/update.json) for browser auto-update polling (~5h cadence). - API telemetry headers on every fetch from the extension; persisted in
noxys-apito power a Console dashboard. - Vite
defineplugin injects build metadata at compile time so it is available in JS without a runtime fetch. - Manifest templating: single
manifest.template.jsonrendered to 4 manifests (chrome+firefox × stable+staging) by a Node script invoked by Vite.
3. Channel separation
| Channel | Chrome ID source | Firefox ID | Distribution | Auto-update mechanism |
|---|---|---|---|---|
| stable | Chrome Web Store key (signed at upload) | noxys@noxys.eu AMO signed | Chrome Web Store + Firefox AMO public listings | Native browser store auto-update |
| staging | Local .pem key → ID derived from SHA256 of public key | noxys-staging@noxys.eu self-signed | GitHub Release .zip (Chrome dev mode load-unpacked) + Firefox temp install OR signed .xpi | R2 update.xml / update.json polled by browser |
Because Chrome derives the extension ID from the public key, the stable and staging builds get different IDs automatically. This means:
chrome.storage.localis automatically namespaced per channel (no collision).- A user can install both side-by-side for QA reproductions.
- Staging builds NEVER show up under the stable listing in the Web Store / AMO.
4. SemVer schema
- Stable:
MAJOR.MINOR.PATCH→1.2.3 - Staging:
MAJOR.MINOR.PATCH-staging.BUILD→1.2.3-staging.045
Chrome Web Store accepts only a 4-segment dot-separated version string. Staging maps to 1.2.3.45 in the manifest, with the channel marker stored separately in build metadata constants (Vite define).
The pre-release suffix -staging.NNN uses NNN = ${{ github.run_number }} (CI auto-incremented).
5. Build metadata embed (Vite define plugin)
// vite.config.ts
define: {
__APP_VERSION__: JSON.stringify(pkg.version), // '1.2.3'
__APP_BUILD__: JSON.stringify(process.env.GITHUB_RUN_NUMBER ?? 'dev'),
__APP_COMMIT__: JSON.stringify(process.env.GITHUB_SHA?.slice(0, 7) ?? 'local'),
__APP_CHANNEL__: JSON.stringify(process.env.NOXYS_CHANNEL ?? 'staging'),
}
A typed src/build-info.ts re-exports these as BUILD_INFO for the rest of the codebase (popup, settings page, API client, telemetry module).
6. Manifest templating
Single manifest.template.json rendered to 4 final manifests via a Node script (scripts/render-manifests.ts) invoked by Vite before bundling. Variables:
name—Noxys(stable) |Noxys (Staging)(staging)version— derived frompackage.json+ channel ruleskey— channel-specific public key (omit for stable; Chrome Web Store assigns)update_url— staging only, points to the R2 update endpointbrowser_specific_settings.gecko.id—noxys@noxys.eu|noxys-staging@noxys.eu
Output: dist/chrome-stable/manifest.json, dist/chrome-staging/manifest.json, dist/firefox-stable/manifest.json, dist/firefox-staging/manifest.json.
7. Telemetry headers
Every API call from the extension carries:
X-Noxys-Extension-Version: 1.2.3
X-Noxys-Extension-Channel: stable
X-Noxys-Extension-Build: 045
X-Noxys-Extension-Commit: a3c4f5b
Persistence in noxys-api (table extension_telemetry):
tenant_id uuid not null references tenants(id)
user_id uuid null references users(id)
version text not null
channel text not null check (channel in ('stable','staging'))
build text not null
commit text not null
last_seen timestamptz not null default now()
count bigint not null default 1
primary key (tenant_id, user_id, version, channel, build)
Row-Level Security: tenant-scoped per existing RLS policy. Privacy: no user-identifying fields beyond what is already in the JWT-derived tenant_id / user_id.
8. Self-hosted update server (staging only)
- Bucket: Cloudflare R2
noxys-extension-staging-updates(EU region). - Public read, CI write via R2 API token.
- Chrome: Google Update Manifest XML at
https://staging-updates.noxys.cloud/chrome/update.xml. - Firefox: JSON manifest at
https://staging-updates.noxys.cloud/firefox/update.json. - Staging manifests include
update_urlpointing at the right endpoint. - Browser polls every ~5h, downloads
.crx/.xpi, auto-installs. - CI workflow updates the
update.xml/update.jsonafter publishing each new artifact.
9. Distribution rules
| Action | Stable | Staging |
|---|---|---|
| Initial install | Chrome Web Store / Firefox AMO public listing | GitHub Release .zip sideload (Chrome dev mode) OR signed .xpi (Firefox) |
| Auto-update | Browser native | R2 update.xml / update.json polled by browser |
| Distribution audience | All Free / Pro / Enterprise tenants | Internal eng + QA + design partners under NDA |
10. Version progression plan
| Phase | Range | Channel | Exit criteria |
|---|---|---|---|
| Pre-alpha | 0.1.0 → 0.4.x | staging | Skipped — bumping straight to 0.5.0 on EXT-VER-1 ship |
| Alpha | 0.5.0 → 0.9.x | staging | 5-10 design partners under NDA, ML-5 F1 ≥ 0.92, MM-GUARD-1, LOG-TIER-1 shipped |
| Beta RC | 1.0.0-rc.1 → 1.0.0-rc.N | staging + stable unlisted | Free tier signup open + ~50 active users + Stripe Billing live + SAS legal entity created |
| GA | 1.0.0 | stable | All P0 acceptance criteria met, launch press, no critical bugs 7 days |
| Maintenance | 1.x.y | stable + staging | Continuous patch / minor releases |
Target dates (best-effort, dependency-driven):
- W21-22 (now): bump
0.1.0 → 0.5.0on EXT-VER-1 ship → Alpha entry. - W23-24:
0.6.x → 0.7.x(ML-5 + ONNX NER + LiteLLM-1/2). - W25-26:
0.8.x → 0.9.x(MM-GUARD-1 + LOG-TIER-1 + LiteLLM dashboard). - W27-28:
1.0.0-rc.1staging + stable unlisted (Stripe + SAS). - W29-30:
1.0.0GA, stable public.
11. Bump rules
- MAJOR (
X.0.0): breaking changes — manifest permission additions, API contract break. - MINOR (
0.X.0): user-visible new feature — MM-GUARD image scanning, LOG-TIER UI, LiteLLM dashboard. - PATCH (
0.0.X): bug fix, UI tweak, perf, security patch (non-breaking). - Pre-release: staging suffix appended automatically by CI (
-staging.${{ github.run_number }}).
Bump triggers:
- Push to
stagingbranch → CI auto-bumps PATCH + appends staging suffix. - Manual
npm version major|minorin PR → on merge tomain, tagextension/v1.2.3triggers stable release workflow. - Pre-merge enforcement: PR check fails if anything under
src/is modified butpackage.json:versionis unchanged. Bypass label:skip-version-bump.
12. User-visible version
- Popup footer:
v1.2.3 stable · build 045 · a3c4f5b(small, monospace). - Settings > About: full metadata table (Version, Channel, Build, Commit, Manifest version, Browser), "Check for updates" button, link to changelog at
noxys.eu/changelog. - Channel badges:
0.x.y staging→ red badge "Alpha — expect bugs"1.0.0-rc.X→ orange badge "Beta"1.0.0+ stable→ no badge
13. Tradeoffs accepted
- Two listings overhead at GA → acceptable for QA isolation pre-launch; revisit at
1.5.0+if maintenance burden grows. - R2 update server adds an ops piece → cost ~0€/month traffic-wise (small bucket, infrequent polls), simple JSON publish from CI.
- Pre-release staging suffix lengthens version strings → cosmetic only; users only see stable versions in the popup.
14. Sovereignty
- All update artifacts hosted on Scaleway/OVH or Cloudflare R2 EU regions — no US data egress for European tenants.
- Telemetry data stored in noxys-api Postgres → tenant-isolated per RLS.
- Chrome Web Store / Firefox AMO = US infrastructure but stable channel only, and the artifacts contain no PII (just signed extension code).
15. References
- Memory:
feedback_three_deployment_methods.md - ADR-010 — Pricing tiers (no version differentiation across tiers)
- ADR-012 — LiteLLM bundled (cross-component telemetry pattern reused here)
- Issue noxys-extension#107 (npm audit chain — versioning helps track patch landings)