Stability & Versioning
This page states what you can rely on, what you cannot, and how a change that
breaks you will reach you. It describes the framework as it is today — Wagoe is
at 1.0.0, and the surface below is the one it promises.
Current status
Current version |
|
Stability |
Stable. Semantic Versioning applies to the public surface — see
From 1.0.0. The breaking changes made during the release
candidates are listed in Before 1.0.0.
Everything deprecated is deferred to |
Artifacts |
31 libraries on Clojars, all in the |
Versioning |
Lockstep — every library carries the same version and is released together |
Java baseline |
JDK 21. Not a recommendation: the installer refuses an older JDK, the images
build and run on 21, and every CI job that starts a JVM uses it.
|
Databases |
PostgreSQL and SQLite, and H2 for tests. MySQL is not supported yet; it is planned after 1.0. Multi-tenancy needs PostgreSQL. |
Lockstep versioning means you never have to work out which wagoe-user goes
with which wagoe-platform: matching version strings are always compatible with
each other. It also means a library gets a version bump when it has not changed,
because something else in the suite did.
The version scheme
Releases went 1.0.0-beta-N, then 1.0.0-rc-N, incrementing N per
release, to 1.0.0. Both replaced the older 1.0.1-alpha-N scheme.
| Last alpha | First beta | Date |
|---|---|---|
|
|
2026-07-19 → 2026-07-23 |
|
Warning
|
The version number went down. Under Maven ordering If you use a version range or any tool that resolves "newest", it will prefer the
last alpha over every beta. Pin exact versions. The |
The old scheme was wrong in a way worth naming, since it is the reason for the
change: 1.0.1-alpha-41 reads as a patch release of a shipped 1.0. Nothing
called 1.0 had ever been released. 1.0.0-beta-N says what is true — work
leading up to a first 1.0.0.
Pre-releases are cut from the next minor
So that it cannot happen again:
-
A pre-release is always
<major>.<minor+1>.0-<stage>-N—1.1.0-alpha-1,1.1.0-rc-2. Never from a patch version. -
Patch versions are only ever final. There is no
1.0.1-alpha-1.
The reason is Maven’s ordering, which puts 1.0.1-alpha-1 above 1.0.0: a
pre-release of a patch outranks the release it precedes, for anyone resolving
"newest". A pre-release of the next minor sorts below that minor and above
everything already out, which is what a pre-release should do.
bb bump refuses the first shape and bb deploy --check-versions — which the
publish workflow runs before any upload — refuses to publish under it.
|
Note
|
|
What counts as public API
Wagoe is hexagonal, and the architecture already draws the line this policy needs. The supported surface is the seam, not the implementation behind it.
| Surface | Where | Supported? |
|---|---|---|
Protocols |
|
✅ Yes — this is the contract |
Schemas |
|
✅ Yes |
Integrant keys |
|
✅ Yes |
Configuration |
Keys read from |
✅ Yes |
CLI + Babashka commands |
|
✅ Yes |
Functional core |
|
❌ No — internal, may change in any release |
Imperative shell |
|
❌ No — internal; depend on the port, not the adapter |
The practical rule: if you can reach it through a protocol, a schema, an
Integrant key or a config key, it is supported. If you are calling into
core/ or shell/ directly, you are using an implementation detail, and
bb check:ports exists precisely to stop the framework’s own code from doing
that.
Where the seam is not a single ports.clj
Most libraries put their protocols in one wagoe.<lib>.ports. Four deviate by
design, and the rule above still applies — only the file layout differs.
| Library | Where its contract lives |
|---|---|
|
|
|
One |
|
Has no |
|
Has no ports and no schema. It is a library of pure Hiccup primitives, so
the public surface is the functions in |
wagoe-mcp is a fifth deviation, but only in naming: its sources live under
wagoe/mcp/ (namespaces wagoe.mcp.*) while the directory is libs/wagoe-mcp.
Its ports namespace is where you would expect.
This is not a hypothetical distinction. wagoe-user dropped five protocols from
its ports.clj in one release, and wagoe-jobs moved a transactional-enqueue
capability from a namespace function onto a port in another. Both were breaking
changes to the supported surface — which is exactly why they belong in a policy
rather than a commit message.
What we promise, and when
Before 1.0.0 (release candidates)
-
The surface froze at
rc-1. After it, a break landed only when a candidate turned one up that had to, and then in a release candidate — except the last three, which landed in1.0.0itself. -
One has, in
1.0.0-rc-2: a:wagoe/*key in:activethat nothing assembles now fails the boot instead of being ignored. Name your own settings blocks in:wagoe/config-keys. -
One more, in
1.0.0-rc-3:bb scaffold generate --cliis gone. It named an interface the scaffolder has never had a generator for, so no value of the flag ever produced one; only a script that passes it is affected. -
Twenty in
1.0.0-rc-4: each is under# BreakinginCHANGELOG.mdwith what to do about it.-
Every route requires a signed-in user unless it says
:public true. -
sortable-thandpaginationswapouterHTMLby default. -
Workflow’s tables ship as a migration, with zoned timestamps; run
migrate upwith replicas stopped. -
:wagoe/logging :levelsets Logback’s levels. -
register-userthrows:validation-errorfor a weak password. -
A Redis provider with no host fails the boot outside dev and test.
-
The workflow API speaks kebab-case JSON with the scaffolded APIs' error shape.
-
The workflow API requires a session or bearer token.
-
Scaffolded APIs and list pages require a signed-in user.
-
A scaffolder
datefield is aDATE. -
The scaffolder refuses unknown field modifiers.
-
bb setupexits 1 on a closed stdin. -
bb setupin an existing project creates no missing config file. -
bb setup --prod truechanges prod only, refuses mock payments, and leaves an AI flag out. -
The admin hard-deletes by default and refuses a parent with has-many rows unless
:on-delete :cascade. -
An unknown admin entity-config key stops startup.
-
:wagoe/payment-providerhas no default, and:mockruns only in dev and test. -
:wagoe/tenantrefuses to boot on SQLite, and on H2 outside a test profile. -
Tenant slugs, schema names, memberships and invite tokens are unique in the database, so a boot stops on existing duplicates.
-
An entity has one instance of each workflow, so
migrate upstops on existing duplicates.
-
-
Three in
1.0.0: under# BreakinginCHANGELOG.mdas well.-
Every JSON error body is
{"error": {"type": …, "message": …}}. -
A scaffolded API refuses a delete that would leave fewer children than
--minwith 409conflict, as the admin does, not 400. -
A write a unique or foreign key refuses is 409
conflict; a scaffolded API answered a missing reference with 400.Twenty-five additions to the three frozen at
rc-1, so the list is twenty-eight.bb check:changelogholds the1.0.0count to the[1.0.0]# Breakingentries, the totals to the counts above, and[Unreleased]to none.
-
-
Breaking changes that already shipped are in
CHANGELOG.mdunder a# Breakingheading, naming the affected library and the migration. -
Deprecations are announced under
# Deprecated. -
We will not remove something in the same release that first deprecates it, unless it is broken enough that leaving it in place is worse. That case is called out explicitly.
From 1.0.0
-
Semantic Versioning, applied to the public surface defined above.
-
A breaking change requires a major version.
-
Anything removed in a major version must have been deprecated in a prior minor version, and must have been deprecated for at least one minor release and 90 days, whichever is longer.
-
Deprecated vars carry
^:deprecatedmetadata with a pointer to the replacement, so your editor andclj-kondocan flag them.
Not every library, though: tiers
The promise above covers stable libraries. A second tier, incubating, is published and usable but outside the breaking-change guarantee, because we already know its API is not finished. Saying so is cheaper for you than a major version bump for a fix we could see coming.
| Library | Why |
|---|---|
|
The Mollie adapter throws "not implemented yet" for |
|
263 lines of source against 37 of test, on the boot path of every application |
|
Five providers, no sweep proving they answer alike |
|
Three geocoding providers, same gap |
|
Three adapters — SMTP, IMAP, Twilio — same gap |
|
Three exporters, same gap |
|
The newest library here, and the least exercised |
|
Its guard and transition API is still moving |
Everything else published is stable. wagoe-tools, wagoe-devtools, wagoe-cli
and wagoe-mcp are tooling and carry no tier — see What is not covered below.
A library is promoted when two things are true: every port with more than one
adapter has a sweep that runs the same assertions against each of them, and its
documentation matches what it does — a README, a page on this site, and an
AGENTS.md that is not describing an older shape. Promotion is not a breaking
change and can happen in any release; demotion cannot happen at all, because a
stable library that turns out to need a break gets the major version instead.
wagoe list modules and wagoe add print the tier, so you learn it before you
depend on it rather than from this page afterwards.
What is not covered
-
wagoe-tools,wagoe-devtools,wagoe-cliandwagoe-mcpare development tooling. They are versioned in lockstep and published to Clojars, but their internal namespaces are not an API — the commands are. -
Generated code. The scaffolder’s output becomes yours; we change the generator, not your files.
-
Database schemas of the framework’s own tables, other than through migrations we ship.
The HTTP server is Jetty
:wagoe/http-server starts Jetty through ring/ring-jetty-adapter (Jetty 12).
That is a decision, not a default that nobody wrote down:
-
It is the adapter the Ring maintainers ship and support themselves, which matters more to a framework with a stability promise than a benchmark does.
-
It carries the standard Ring WebSocket API that
wagoe-realtimeis built on, HTTP/2, async handlers for streaming responses, and a virtual-thread executor. Nothing on the roadmap needs a capability Jetty lacks. -
The alternatives each cost something without solving a problem we have: http-kit has its own WebSocket API and no HTTP/2; Aleph brings Netty and a second concurrency model; Undertow adds nothing over Jetty 12.
What this means for the promise: there is no server port. :wagoe/http-server
is Jetty, its options map is Jetty’s, and swapping the server is not a supported
configuration. If a real need for another server appears, the answer is a port
with a conformance suite — the same route every other adapter in the framework
takes — not a config flag.
Deprecation, in practice
A deprecation is three things, and it is not a deprecation without all three:
-
^:deprecatedmetadata on the var, protocol or method, with a docstring line naming the replacement. -
A
# Deprecatedentry inCHANGELOG.mdfor the release that introduced it. -
A replacement that exists and is documented at the time of the announcement.
(defn ^:deprecated enqueue-in-tx!
"DEPRECATED: use the ITransactionalQueue port instead.
Removed no earlier than the next major release."
[& args]
...)
An Integrant component key cannot carry metadata, so for one the first point is
a WARN on init naming the replacement. A changelog entry that the running
system says nothing about is an announcement to whoever already read it.
CI enforces the second point: a var carrying ^:deprecated that CHANGELOG.md
never names fails the build.
Deferred to 2.0
Everything currently deprecated, and what replaces it. None of these is removed
in a 1.x release. What 2.0 and the 1.x releases before it will build is on
the roadmap.
| Deprecated | Use instead |
|---|---|
|
|
The |
Nothing — admin’s components take the settings map directly. |
The |
|
|
|
HTTP API versioning is a separate thing
Do not confuse library stability with the HTTP API versioning your application exposes to its own clients. Those are different axes and Wagoe handles them separately.
For your endpoints, the platform ships version lifecycle support — deprecated
version sets, sunset dates, and an X-API-Deprecated response header — in
wagoe.platform.shell.http.versioning. Routes are mounted under /api/v1 by
default. That machinery governs the contract between your app and its
clients; this page governs the contract between Wagoe and you.
Reporting a break
If a release breaks you and the break is not in CHANGELOG.md, that is a bug in
this policy as much as in the code. Open an issue on
GitHub with the two versions and the failure —
an unannounced break gets a fix or a revert, not a workaround.
See also
-
Roadmap — what is left before
1.0.0, and what comes after -
Functional Core / Imperative Shell — why the
core/↔shell/split is the stability boundary