Liking cljdoc? Tell your friends :D

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 its release candidate for 1.0.0, and the surface below is the one 1.0.0 will promise.

Current status

Current version

1.0.0-rc-1

Stability

Release candidate. The 1.0.0 surface is frozen: no breaking change lands between 1.0.0-rc-1 and 1.0.0. Everything deprecated is deferred to 2.0 — see Deferred to 2.0.

Artifacts

31 libraries on Clojars, all in the com.wagoe group. Artifact ids are wagoe- followed by the library name — wagoe-core, wagoe-platform — except where the library name already begins with wagoe-, which is not doubled: libs/wagoe-cli publishes as com.wagoe/wagoe-cli, not wagoe-wagoe-cli, and likewise for wagoe-mcp. The authoritative id is the (def lib …) coordinate in each library’s build.clj, which is what bb deploy reads.

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. bb check:jdk holds those to one number, so a JDK written in one place and not another cannot ship.

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 were 1.0.0-beta-N, incrementing N per release, and are now 1.0.0-rc-N on the way to 1.0.0. Both replaced the older 1.0.1-alpha-N scheme.

Table 1. The alpha → beta transition
Last alphaFirst betaDate

1.0.1-alpha-42

1.0.0-beta-1

2026-07-19 → 2026-07-23

The version number went down. Under Maven ordering 1.0.0 sorts below 1.0.1, so 1.0.0-beta-1 compares as older than 1.0.1-alpha-42 even though it shipped four days later.

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 1.0.1-alpha-* line is discontinued and receives no fixes.

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>-N1.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.

CHANGELOG.md has no separate 1.0.0-beta-1 entry. Its changes were still sitting in [Unreleased] when 1.0.0-beta-2 shipped eight days later, so they are recorded under the 1.0.0-beta-2 heading. Nothing is missing; it is filed one heading down.

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.

SurfaceWhereSupported?

Protocols

wagoe.<lib>.ports, or the library’s documented equivalent — see the exceptions below. Presence is enforced by bb check:ports.

✅ Yes — this is the contract

Schemas

wagoe.<lib>.schema — Malli schemas for entities and inputs, where the library defines them

✅ Yes

Integrant keys

:wagoe/* component keys and their init options

✅ Yes

Configuration

Keys read from resources/conf/<env>/config.edn and their environment variables

✅ Yes

CLI + Babashka commands

wagoe …, and the bb tasks documented on this site

✅ Yes

Functional core

wagoe.<lib>.core.*

❌ No — internal, may change in any release

Imperative shell

wagoe.<lib>.shell.*, including every adapter

❌ 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.

LibraryWhere its contract lives

platform

wagoe.platform.ports. — a directory rather than one file. Today it holds ports.http (IHttpServer; routing is not a port, see ADR-037). Its DBAdapter and IFileSystemAdapter protocols currently sit under shell.adapters..protocols; they are supported surface despite the location, and moving them under ports is tracked work

observability

One ports.clj per concern: wagoe.observability.{metrics,tracing,errors,logging}.ports

cache

Has no core/ at all — a thin adapter library, so ports plus shell is the whole of it

shared-ui

Has no ports and no schema. It is a library of pure Hiccup primitives, so the public surface is the functions in wagoe.shared.ui.core.* themselves. Treat those the way you would a port; they do not follow the "`core/` is internal" rule, because for this library there is nothing else.

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

Now (release candidate)

  • The surface is frozen. 1.0.0 is 1.0.0-rc-1 plus whatever the candidate turns up; nothing further lands under # Breaking before the tag.

  • Breaking changes that already shipped are in CHANGELOG.md under a # Breaking heading, 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 ^:deprecated metadata with a pointer to the replacement, so your editor and clj-kondo can 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.

Table 2. Incubating at 1.0.0
LibraryWhy

payments

The Mollie adapter throws "not implemented yet" for create-off-session-payment and expire-checkout-session, so the PSP adapters are not interchangeable — which is the whole point of the port

config

263 lines of source against 37 of test, on the boot path of every application

ai

Five providers, no sweep proving they answer alike

geo

Three geocoding providers, same gap

external

Three adapters — SMTP, IMAP, Twilio — same gap

reports

Three exporters, same gap

audience

The newest library here, and the least exercised

workflow

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-cli and wagoe-mcp are 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-realtime is 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:

  1. ^:deprecated metadata on the var, protocol or method, with a docstring line naming the replacement.

  2. A # Deprecated entry in CHANGELOG.md for the release that introduced it.

  3. 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.

DeprecatedUse instead

:provider :in-memory, :redis-streams, :database

:memory, :redis, :db. The old names still work and warn.

The :wagoe/admin Integrant component

Nothing — admin’s components take the settings map directly. :wagoe/admin under :active is a different thing and is not deprecated; it is how an application switches the module on.

The :wagoe/auth-user-repository component

:wagoe/user-repository, which :wagoe/auth-service already takes.

wagoe.jobs.shell.adapters.db/enqueue-in-tx!

wagoe.jobs.ports/enqueue-in-tx! on the queue component, so callers stay on the port rather than knowing which adapter they have.

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

  • Functional Core / Imperative Shell — why the core/shell/ split is the stability boundary

  • CHANGELOG

Can you improve this documentation? These fine people already did:
thijscreemers & Thijs Creemers
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close