Software versioning for web, mobile, and browser extensions
Software versioning strategies for SaaS: SemVer, app versioning on stores, OTA vs binaries, and how to version web, mobile, and extensions without chaos.

Most teams treat software versioning as a package.json chore until a store rejects an upload, a crash report says "unknown build," or support asks which release a customer is on. Then it suddenly matters.
Libraries have Semantic Versioning. Mobile stores have marketing versions and build numbers. Browser extensions burn a version string forever once you upload it. Web apps can redeploy silently and skip the whole conversation until something breaks in production.
If you ship a SaaS across more than one surface, you need a strategy that fits each platform without forcing one fake "v1.0.0 everywhere" policy.
Short answer
Software versioning is how you label a release so humans, stores, and tooling can tell builds apart. Use SemVer as a shared language (MAJOR.MINOR.PATCH), then adapt per platform: a product version for web, marketing version + monotonically increasing build numbers for mobile, and a never-reuse manifest version for browser extensions. Keep surfaces independent when release cadences differ. Tie OTA mobile updates to a runtime, not to every git commit.
Quotable definition: Software versioning assigns a structured identifier to a shippable cut of your product so support, stores, analytics, and rollbacks can refer to the same thing.
Why software versioning still matters
Git already has SHAs. Hosts already have deploy IDs. Those are engineer-facing. Versioning is the product-facing label.
You want it for:
| Job | Without a version | With a version |
|---|---|---|
| Support | "I refreshed yesterday" | "I'm on 1.8.2" |
| Monitoring | Group by hour and pray | Group by release |
| Stores | Rejected upload / silent fail | Accepted, attributable build |
| Rollbacks | Guess which deploy | Restore a named cut |
| Changelogs | Vague "latest" | Diff people can read |
SemVer is the default vocabulary because it encodes kind of change, not just sequence. From the SemVer 2.0.0 spec:
- MAJOR - incompatible API (or, for apps, changes users must treat as a new contract)
- MINOR - backward-compatible functionality
- PATCH - backward-compatible fixes
For end-user apps the "public API" is fuzzier than a library. Treat installation requirements, permissions, and breaking UX as the contract users and stores care about. The number still works as communication even when you are not publishing npm packages.
Three versioning strategies (pick intentionally)
Teams usually land on one of these. Mixing them without documenting the rule is how monorepos get confusing.
1. Strict SemVer for shared packages
Best for libraries, SDKs, and anything with downstream consumers who read changelogs programmatically. Breaking export? Major. New optional API? Minor. Bugfix? Patch.
In a Turborepo, that often means @workspace/* packages can stay on their own versions (or 0.x internally) while apps carry the product versions users see.
2. Product / marketing SemVer for apps
Best for web apps, mobile store listings, and extensions. 2.4.0 means "this is the product release," not "this npm graph moved." Patch/minor/major still guide the team; the audience is support and users, not npm install.
3. CalVer or build-only labels
Date-based (2026.07.29) or sequential build IDs work for internal tools and some web-only products. They are weaker for communicating break risk. Fine as a deploy stamp alongside a human product version; weak as the only public label if you also ship mobile and extensions.
Rule of thumb: one human product version per deployable app, plus whatever opaque integers the platform demands underneath.
How each platform supports (and constrains) versioning
This is where generic "just use SemVer" advice falls over.
Web
Web is the freest platform. Nothing stops you from shipping without bumping a number. That is also why web teams skip it.
What good looks like:
- A single product version for the app users meet
- That value shown somewhere boring (footer, about screen) and sent to error/analytics tools
- Bump when the release is worth naming, committed with the production deploy
- Git SHA or host deployment ID as the engineering breadcrumb in addition, not instead
You do not get store rejection emails. You do get "which build had the checkout bug?" emails. Versioning is how you answer them. Deeper kit notes: web versioning recipe.
Mobile (iOS and Android)
Mobile is stricter because Apple and Google gate uploads, and users keep old binaries for months.
You almost always manage two identifiers:
| Identifier | Example | Job |
|---|---|---|
| Marketing / user-facing version | 1.6.2 | What humans and store listings show |
| Build number (iOS) / version code (Android) | 47 | Strictly increasing integer per upload |
You can upload builds 48, 49, 50 all labeled 1.6.2 while iterating a release. You cannot reuse a lower build number the store already accepted.
Mobile also splits release types:
| Change | Typical path |
|---|---|
| JS/UI-only fixes | Over-the-air update on the same marketing version / runtime |
| Native modules, SDK bumps, permissions, icons | New store binary; bump marketing version when the native contract changes |
OTA is not a free pass to ignore versioning. Updates must stay compatible with the native binary they land on (Expo's runtimeVersion is one way to encode that; see EAS app versions and our mobile updates guide). Practical playbook: mobile versioning recipe.
Browser extensions
Extensions sit closer to mobile than web. Chrome, Edge, and Firefox treat the manifest version as a one-shot id for that listing. Reuse a version and the upload fails. Decrease it and you fight the store.
That means:
- Every zip you ship needs a new version string
- Burned a bad upload? Bump again. History does not forget
- Permission or host-access changes raise review cost; encode that in at least a minor bump and say so in listing notes
Frameworks like WXT typically derive manifest version from package.json so you do not maintain two sources. Kit notes: extension versioning recipe.
Cross-platform SaaS: do versions have to match?
Short answer: no.
A Next.js deploy every day, an App Store review every two weeks, and an extension permission review that takes longer will never share one number honestly. Forcing web === mobile === extension creates fake alignment and delayed releases.
What should stay aligned instead:
- API contracts and auth/billing rules (one backend; see cross-platform SaaS with one backend)
- Support playbooks that ask for platform + version (
iOS 1.6.2 (build 50),extension 1.2.5,web 1.8.2) - Minimum supported client versions when you break the API on purpose
Independent product versions per app, shared backend versioning discipline for the API you actually break.
A practical strategy you can run Monday
- Name the product version for each app (
apps/web,apps/mobile,apps/extensionin a monorepo). - Agree SemVer meanings for apps: patch = fix, minor = feature, major = breaking UX / permissions / install requirements.
- Automate platform integers where possible (remote build numbers on mobile; never hand-edit if your CI already increments).
- Decide OTA vs store before you bump mobile. JS-only → OTA. Native → store + version bump.
- Ship notes with every store/extension upload. Reviewers and users read them; future-you does too.
- Attach the version to telemetry so crashes and funnels group by release, not by calendar luck.
- Document the oldest client you still support so API deprecations are not surprises.
Feature flags pair well with this. A version says what binary or bundle someone has; a flag says which experiment they see inside it. Do not replace versioning with flags. Use both.
How we approach it in TurboStarter
TurboStarter is a multi-platform monorepo (Next.js web, Expo mobile, WXT extension) with one backend. Versioning follows the platform constraints above instead of one global counter:
- Web - product version in
apps/web/package.json, exposed viaappConfig, shown in the footer - Mobile - user-facing version in Expo config, EAS remote
appVersionSourcewith productionautoIncrement,runtimeVersionpolicyappVersionso OTA stays scoped - Extension - version in
apps/extension/package.json; WXT writes the manifest; stores get a fresh number per upload
Same SemVer language, different enforcement. Recipes go deeper per surface: web, mobile, extension. For the broader architecture, the Next.js AI SaaS starter kit and React Native SaaS starter pages show how those apps sit in the monorepo.
Mistakes that waste weeks
- One version for the whole monorepo - a docs typo bump should not force an App Store submission.
- Bumping mobile
versionto push an OTA - you just changed the runtime contract; old binaries may never see the update you meant. - Reusing extension versions - the store remembers; rebuild with a new number.
- Only tracking git SHAs on web - fine for engineers, useless for most support channels.
- Ignoring permissions as a versioning event - extension and mobile trust-boundary changes deserve a louder bump than a CSS fix.
- Skipping release notes - future debugging becomes folklore.
Frequently asked questions
Git tags point at commits. Software versioning labels a shipped artifact users or stores receive. Tags often mirror versions (v1.8.2), but the version has to live in the artifact metadata too (package.json, Expo config, extension manifest) or clients never see it.
Yes as a team language, even though no store enforces it. SemVer tells teammates whether a release is a fix, a feature, or a break. Pair it with deploy IDs for engineering forensics. See semver.org.
App versioning usually means a user-facing marketing version plus a platform build number / version code that must increase on every store upload. Keep marketing versions aligned across iOS and Android for the same release when you can; let build integers differ per platform rules.
No. OTA updates target a runtime compatible with the installed binary. Bump the store marketing version when the native contract changes, not for every JS hotfix. Details: Expo EAS app versions and TurboStarter mobile updates.
They can, but they usually should not. Release cadences differ. Prefer independent product versions per app and a clear support format that includes the platform name.
Each upload needs a new manifest version for that listing. Versions are not reusable. Permission changes often trigger extra review, so treat them as meaningful bumps. WXT maps package.json version into the manifest on build.
Takeaway
Software versioning is not bureaucracy. It is how multi-platform products stay debuggable when web redeploys daily, mobile ships through stores, and extensions burn version strings forever. Use SemVer as the shared dialect, respect each platform's constraints, keep app versions independent when cadences diverge, and wire the label into UI, telemetry, and release notes so the number means something when something breaks.
Self-host your Next.js Turborepo app with Docker in 5 minutes
Learn how to containerize your Next.js Turborepo app with Docker, optimize the image, and deploy it to any environment in minutes with practical examples.
Stripe vs Lemon Squeezy vs Polar vs Creem - best SaaS payment provider (2026)
We compared fees, MoR tax handling, dev experience & subscriptions. See which provider fits indie hackers vs B2B SaaS.


