Skip to main content

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 separate beta channel — beta phase = staging with 1.0.0-rc.N versions).
  • 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 .zip for 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-api to power a Console dashboard.
  • Vite define plugin injects build metadata at compile time so it is available in JS without a runtime fetch.
  • Manifest templating: single manifest.template.json rendered to 4 manifests (chrome+firefox × stable+staging) by a Node script invoked by Vite.

3. Channel separation

ChannelChrome ID sourceFirefox IDDistributionAuto-update mechanism
stableChrome Web Store key (signed at upload)noxys@noxys.eu AMO signedChrome Web Store + Firefox AMO public listingsNative browser store auto-update
stagingLocal .pem key → ID derived from SHA256 of public keynoxys-staging@noxys.eu self-signedGitHub Release .zip (Chrome dev mode load-unpacked) + Firefox temp install OR signed .xpiR2 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.local is 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.PATCH1.2.3
  • Staging: MAJOR.MINOR.PATCH-staging.BUILD1.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:

  • nameNoxys (stable) | Noxys (Staging) (staging)
  • version — derived from package.json + channel rules
  • key — channel-specific public key (omit for stable; Chrome Web Store assigns)
  • update_url — staging only, points to the R2 update endpoint
  • browser_specific_settings.gecko.idnoxys@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_url pointing at the right endpoint.
  • Browser polls every ~5h, downloads .crx / .xpi, auto-installs.
  • CI workflow updates the update.xml / update.json after publishing each new artifact.

9. Distribution rules

ActionStableStaging
Initial installChrome Web Store / Firefox AMO public listingGitHub Release .zip sideload (Chrome dev mode) OR signed .xpi (Firefox)
Auto-updateBrowser nativeR2 update.xml / update.json polled by browser
Distribution audienceAll Free / Pro / Enterprise tenantsInternal eng + QA + design partners under NDA

10. Version progression plan

PhaseRangeChannelExit criteria
Pre-alpha0.1.0 → 0.4.xstagingSkipped — bumping straight to 0.5.0 on EXT-VER-1 ship
Alpha0.5.0 → 0.9.xstaging5-10 design partners under NDA, ML-5 F1 ≥ 0.92, MM-GUARD-1, LOG-TIER-1 shipped
Beta RC1.0.0-rc.1 → 1.0.0-rc.Nstaging + stable unlistedFree tier signup open + ~50 active users + Stripe Billing live + SAS legal entity created
GA1.0.0stableAll P0 acceptance criteria met, launch press, no critical bugs 7 days
Maintenance1.x.ystable + stagingContinuous patch / minor releases

Target dates (best-effort, dependency-driven):

  • W21-22 (now): bump 0.1.0 → 0.5.0 on 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.1 staging + stable unlisted (Stripe + SAS).
  • W29-30: 1.0.0 GA, 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 staging branch → CI auto-bumps PATCH + appends staging suffix.
  • Manual npm version major|minor in PR → on merge to main, tag extension/v1.2.3 triggers stable release workflow.
  • Pre-merge enforcement: PR check fails if anything under src/ is modified but package.json:version is 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)