Versioning

Strict-forward semver for an App — which bump to use, why version must move forward on every successful build, and why a breaking change means a new capability key and a new route.

version in manifest.json is semver, and versioning is strict-forward: a published contract may gain things, never change the meaning of what it already published.

BumpFor
Patch (1.0.01.0.1)Bug fixes and internal changes. No contract change.
Minor (1.0.01.1.0)New capabilities, new optional request fields, new declared statuses, new entities and columns.
Major (1.0.02.0.0)Anything a caller could notice as a break.

Bump on every successful build

version is what identifies the artefact in the registry, so every successful build needs a version nobody has used before. Deploying the same version twice is not how you ship a fix; bump it, even for a one-character change to a log line.

Practically, that means the bump is part of the deploy, not an afterthought:

Terminal
# 1. bump `version` in manifest.json
# 2. then:
revenexx deploy app

Two consequences worth internalising:

  • A version number is cheap. 1.4.17 is not embarrassing; a re-used version is a build you cannot identify later.
  • Bump honestly. The number is how a merchant and a dependent app reason about your app. A breaking change shipped as a patch is a lie the semver range in somebody else's manifest will believe.

A breaking change is a new capability and a new route

This is the rule that follows from strict-forward, and it is the one that shapes your API:

A breaking change is a new major and a new capability key and a new route.

You do not change the meaning of serials.list@1 at GET /v1/serials. You define a new capability on its own path, and both serve traffic until every caller has moved:

manifest.capabilities.json
[
  {
    "type": "define",
    "capability": "serials.list",
    "version": "1.0.0",
    "route": { "method": "GET", "path": "/serials" },
    "response": { "title": "SerialList", "type": "object" }
  },
  {
    "type": "define",
    "capability": "serials.search",
    "version": "1.0.0",
    "summary": "Search serials — replaces serials.list",
    "route": { "method": "GET", "path": "/serials/search" },
    "response": { "title": "SerialSearchResult", "type": "object" }
  }
]

There is no in-place breaking change, and no way to retire a route out from under a tenant that still calls it. That constraint is what makes the published SDKs safe to pin, and it is the reason a tenant on an older version of your app keeps working whether or not anybody upgrades them.

Deprecating the old one is a documentation act, not a technical one: say so in the old capability's description, point at the replacement, and leave the route serving traffic.

What is a break

The test is not "did the schema change" but "could a caller written against the old contract notice?"

Safe — minor:

  • A new capability.
  • A new optional request field.
  • A new field in a response. (Callers ignore what they do not know.)
  • A new declared status in responses that the operation could already return.
  • A new entity, a new nullable column, a new index.
  • A new setting with a default.

Breaking — new capability key, new route:

  • Removing or renaming a request or response field.
  • Making an optional request field required, or narrowing an enum.
  • Changing a field's type, or the meaning of its value.
  • Changing which status the operation answers on success.
  • Changing the route's path or method.
  • Renaming the capability key.

The two sneaky ones:

Tightening the declared request schema is a break even though it looks like validation hygiene. The gateway validates before your function runs, so a newly-required field turns a working caller's request into a 400. See Errors.

Changing a summary or a description is not a break — but it is the only documentation your callers have, so it is worth a version of its own rather than waiting for one. See The published OpenAPI document.

Schema changes obey the same instinct

The platform enforces it for you: additions apply, and a removal needs an explicit tombstone. A column dropped from schema.json stays in the database with its data.

So a data-model change is normally a minor, and the awkward cases — a rename, a type change — are the three-deploy dance rather than a major. See Migrations.

Dependency ranges

manifest.json
"dependencies": { "revenexx/markets": "^0.1", "revenexx/orders": ">=0.6" }

^ is the right default: it picks up fixes and features and stops at the next major, where breaking changes are allowed to land. An exact pin never changes underneath you and also never gets a fix until you bump it by hand. See Dependencies.

The same logic applies to a capability's compatible range when you call another app or implement its contract.

What we do not claim about upgrades

Whether and when an installed tenant moves from one version of your app to the next is an operator and Cockpit concern, and the rules are not something we can state accurately today. Do not design around an assumed auto-upgrade window, and do not assume a tenant will be on your latest version.

What you can rely on is the contract discipline: because a breaking change is always a new capability key on a new route, a tenant on an older version keeps working whether or not anyone upgrades them. That is the property to design for.

Next steps

Was this page helpful?