Semver for SaaS: Why the Library Model Breaks (and What to Use Instead)
Semantic Versioning was built for libraries. For SaaS products, it leaks, confuses, and undercommunicates. Here's a versioning scheme that actually works for continuous delivery.
Table of Contents
- What semver was designed for
- Where semver leaks for SaaS
- 1. There is no "version" your customer consumes
- 2. Breaking changes rarely correspond to versions
- 3. Your API version and your product version diverge
- 4. Continuous delivery shatters the model
- The versioning schemes teams actually reach for
- Option A: Drop versioning entirely
- Option B: CalVer (date-based versioning)
- Option C: "Season" or named releases
- Option D: Fake semver
- The proposal: Release Train versioning
- Examples
- The rules
- Why this works
- What Stripe, Linear, and Shopify actually do
- What to do this week
- FAQ
- Wrap-up
Open any npm package's CHANGELOG.md and you'll see something like v2.4.7 → v2.4.8. Patch. Bug fix. Zero breaking changes, zero migration. A developer reading this knows exactly what to expect because Semantic Versioning — semver — is a contract: major for breaking, minor for additive, patch for fixes.
Now open your B2B SaaS's changelog. What version is it on? If your answer is "uh, we don't really version", you're the 95%. If your answer is "v34.2.1", you're the 5% pretending semver still works for a web product that deploys 20 times a day.
This essay explains why semver in its canonical form breaks for SaaS, why the alternatives most teams reach for are worse, and proposes a pragmatic versioning scheme that keeps the communication value without the library-shaped overhead.
What semver was designed for
Semver was published as a spec by Tom Preston-Werner in 2011. The target was package ecosystems — npm, RubyGems, Maven, Cargo — where:
- Versions are immutable artifacts. Once
v2.4.7is published, it never changes. - Consumers pin versions. Your
package.jsonsays"lodash": "^4.17.21". - A new major version is consumed by choice. You upgrade when you're ready.
- The version number is the API contract between publisher and consumer.
Under these constraints, semver is a beautiful signal. v3.0.0 landing means the library author broke something; v2.5.0 means they added something. Consumers can automate decisions (npm audit, Dependabot, Renovate) based on the version shape alone.
Where semver leaks for SaaS
A hosted B2B SaaS violates every one of those assumptions.
1. There is no "version" your customer consumes
Your customer doesn't download a binary. They hit app.yourcompany.com. Whatever version is deployed at the moment of the request is the version they get. You can't ship v2.4.7 and leave v2.4.6 running for users who haven't upgraded — there is no upgrade.
This makes "major version" meaningless. What would v3.0.0 of a hosted dashboard even mean? The dashboard changes. Users don't opt in.
2. Breaking changes rarely correspond to versions
In a library, a breaking change is an API signature change. In a SaaS product:
- Moving a button from top-right to bottom-left breaks a workflow but isn't an API change.
- Renaming a feature ("Teams" → "Workspaces") breaks external documentation but not code.
- Deprecating a pricing tier breaks revenue expectations for customers mid-contract.
None of these fit the semver definition of "breaking". Yet any of them can be more disruptive than a proper API break.
3. Your API version and your product version diverge
Most SaaS with public APIs version the API separately (/v1/, /v2/) and leave the product unversioned. This is correct — the API is the library-shaped thing, the product UI is the SaaS-shaped thing. But then what is the "version" in your changelog? You have two, or none.
4. Continuous delivery shatters the model
A serious SaaS ships multiple times per day. A changelog with v47239.1.2 is absurd. Bumping to v47240.0.0 for a breaking change hides inside the noise. The version number's role as a signal is erased by the cadence.
The versioning schemes teams actually reach for
When semver stops working, teams drift into one of four alternatives. Three of them are bad.
Option A: Drop versioning entirely
The most common answer. The changelog is a list of dated entries, no version numbers attached. Entries are identified by date + title.
Pros: Honest, matches how the product actually ships. Cons: Users lose the ability to refer to "the state of the product on" a specific version. Support becomes harder ("which version were you on when this happened?" — uh, it's a hosted product).
Option B: CalVer (date-based versioning)
Version is the date: 2026.04.22. Popular with Ubuntu (24.04), JetBrains (2026.1), Postmark, Sentry.
Pros: Unambiguous ordering. Implicit cadence signal. No tortured debate over what "major" means.
Cons: No information about the nature of the change. 2026.04.22 and 2026.04.23 might differ by a typo fix or an entire product rewrite. The version doesn't help the reader triage.
Option C: "Season" or named releases
Figma's "Config 2025" release. Notion's "AI release". Version is a name, not a number.
Pros: Marketing-friendly, memorable, bundles naturally with campaigns. Cons: Doesn't scale to weekly cadence. Can't be sorted. Users can't answer "am I on the latest?".
Option D: Fake semver
Keep incrementing v2.x.y forever. Patch for bug fixes, minor for features, major for marketing moments. The numbers don't mean what semver says they mean.
Pros: Familiar shape. Cons: Actively misleading. A "major" version bump that isn't a breaking change trains users to ignore the signal. Worse than no versioning.
The proposal: Release Train versioning
We've seen the best results with a hybrid we'll call Release Train versioning — CalVer for sequencing, semver-style semantic suffixes for signal. Used by a handful of serious SaaS products (Stripe's API uses a variant, Shopify's platform uses a variant).
The format:
YYYY-MM-DD[.N][-signal]
Where:
YYYY-MM-DDis the release date..Nis an optional sub-index for multiple releases on the same day (2026-04-22.2).-signalis an optional semantic suffix:-breaking,-security,-preview.
Examples
2026-04-22— routine weekly release, no special signal.2026-04-22.2— second release on the same day, usually a hotfix.2026-04-22-breaking— release includes a change users must action.2026-04-22-security— security release, separate from routine features.2026-05-15-preview— release gated behind a preview flag.
The rules
- Every customer-facing change gets a Release Train entry. No unversioned entries in the changelog.
- The signal suffix is mandatory when applicable. A breaking change without
-breakingis a process failure, not a style choice. - The API keeps its own semver.
/v1/and/v2/paths are untouched. The Release Train is the product version, not the API version. They evolve independently. - Support, docs, and status pages all quote Release Train. "This was resolved in 2026-04-18-security" is a referenceable, sortable, unambiguous statement.
Why this works
- Sortable. Chronological order is the version order. No confusion about what came first.
- Signals without lying.
-breakingmeans what it says. The absence of a suffix means routine. - Survives continuous cadence. There's no collision between "daily ship" and "version discipline" — every day can have its own release train, every day can have zero, without the version scheme breaking.
- Humans can read it.
2026-04-22-breakingis both a date and a warning. No prior context required.
Migration path. If you're already on a semver-shaped version like v2.4.7, don't flip cold. Freeze the semver on whatever today's value is, add a Release Train suffix for 6 weeks (v2.4.7 / 2026-04-22), then drop the semver prefix once users have adapted. Parallel running keeps support tickets searchable.
What Stripe, Linear, and Shopify actually do
We read the versioning schemes used by three widely respected platforms. None of them use canonical semver for the product. All of them use date-anchored versioning with a separate, explicit API version.
| Platform | Product version | API version |
|---|---|---|
| Stripe | Dated API versions (2026-04-15) | Same — the API is the product |
| Linear | Unversioned public changelog, dated entries | GraphQL, versioned via schema evolution |
| Shopify | API version labeled by quarter (2026-04) | Same, explicit API version headers |
| Notion | Named releases + dates | Simple /v1/ with additive-only evolution |
| Figma | Named releases + dates | REST + plugins have separate semver |
The pattern: date-driven for product, explicit separate versioning for the API surface. Release Train is a generalization of this pattern.
What to do this week
- Decide: do you have an API that customers code against?
- Yes → keep or introduce real semver for the API, separate from the product.
- No → skip the API versioning question entirely.
- Write down your current product "version" scheme. If the answer is "we don't have one", that's fine, but acknowledge it.
- Pick one scheme and commit. Release Train is our recommendation. CalVer is acceptable. Unversioned dated entries are acceptable if you never need to reference "the version" in support or docs.
- Document the rules in your
CHANGELOG.mdheader. Two paragraphs. Every new engineer and every customer should be able to read what your version means. - Audit the last 10 entries. Back-fill versions using your new scheme so the history is consistent.
FAQ
Wrap-up
Semver is a triumph of specification — for the ecosystem it was designed for. Forcing it onto a SaaS product that ships continuously is a category error. The version number becomes noise, the signal dies, and teams eventually give up on versioning altogether.
Release Train — date-anchored, signal-suffixed, separate from API semver — keeps what semver was for (clarity about change nature) without the library-shaped assumptions. Four rules, no tortured "what counts as major" debates, and a timeline your customers can actually read.
Your API, if you have one, still gets real semver. That's the library-shaped surface of your product. The rest is a timeline, and that's fine.