Every Clojure project I began started the same way. Wiring Integrant to Reitit to next.jdbc took a day. Then came auth, password reset, migrations, config per environment, error handling, CI, and the first module everyone else would copy. Two weeks before the first line of actual business logic. I got tired of it. So I built Wagoe.
The problem
Clojure has great libraries. What it doesn’t have is a way to make the decisions between them stick.
Where does business logic live? Where does I/O go? How do keys get from snake_case in the database to kebab-case in your maps? Every team answers these. Usually once, usually in someone’s head. Then a new dev joins and the answers drift.
Wagoe writes them down. And then enforces them.
One rule
{module}/
├── core/ ; pure functions. no I/O, no logging, no exceptions
├── shell/ ; side effects: persistence, services, HTTP
├── ports.clj ; protocols, the contract between the two
└── schema.clj ; Malli
core never touches shell. That’s it. Functional Core, Imperative Shell.
Not my idea: Gary Bernhardt and Alistair Cockburn got there first. The difference is that Wagoe does not document the rule. It enforces it. Import a DB or logging library into core/ and your build breaks.
What your code looks like:
(def status-transitions
{:pending #{:paid :cancelled}
:paid #{:shipped :refunded :cancelled}
:shipped #{:delivered :refunded}
:delivered #{:refunded}})
(defn transition-status [order new-status now]
(if (contains? (status-transitions (:status order)) new-status)
{:ok (assoc order :status new-status :updated-at now)}
{:error :invalid-transition :from (:status order) :to new-status}))
Data in, data out. Test it with (transition-status order :paid now). No mocks, no database, no ceremony.
What’s in the box
28 application libraries, plus the CLI and developer tooling: 31 artifacts under com.wagoe, all on the same version. Here’s the short version:
(def libs
{:foundation [:core :platform :observability :config :events]
:auth [:user] ; JWT, sessions, MFA
:data [:admin :storage :cache :search :jobs :email]
:b2b [:tenant :workflow :reports :calendar :payments :geo :external]
:reach [:realtime :push :audience :i18n]
:ui [:ui-style :shared-ui]
:ai [:ai :mcp :scaffolder]})
;; (->> libs vals (map count) (reduce +)) ;; => 28
A few highlights:
-
wagoe-admin: a CRUD admin UI generated from your Malli schemas. Hiccup + HTMX, no build step. -
wagoe-tenant: multi-tenancy with schema-per-tenant isolation on PostgreSQL. -
wagoe-jobs: durable background jobs with retries and a dead letter queue. -
wagoe-workflow,wagoe-reports,wagoe-push:defworkflow,defreport,defpush. Declare it as data and let the library do the rest.
Each module is a plain deps.edn dependency. Take just wagoe-core, make your pick, or take everything. No lock-in.
AI that doesn’t have to guess
bb scaffold ai "invoice module: number, customer, line items, total in cents, due date, status draft→sent→paid"
You get a Malli schema, pure core functions, a ports protocol, a service, a persistence adapter, HTTP handlers and a test namespace, all wired into the Integrant config.
This works because the structure is predictable. Every library ships an AGENTS.md, every module has the same shape, and there’s an MCP server. I put it this way at Clojure/Conj:
The agent didn’t get smarter. The framework stopped asking it to guess.
Prefer to drive it yourself? bb scaffold gives you an interactive wizard instead.
"Isn’t a framework un-Clojurian?"
Fair question. The real choice is not framework vs. no framework. It’s explicit conventions vs. implicit ones. Every serious Clojure backend already has conventions. Wagoe makes them visible, consistent and enforced.
Don’t like the word framework? Call it an opinionated starting point. Same thing.
Try it
curl -fsSL https://get.wagoe.org | bash
wagoe new my-app
cd my-app
clojure -M:migrate up
source .env
clojure -M:repl
(go) ;; => System started. Visit http://localhost:3000
Then (reset) after every change. REPL-driven, the way it should be.
Docs: wagoe.org · Source: github.com/wagoebv/wagoe (samples included)
The honest part
1.0: the public APIs are stable, and breaking one is a versioning event. Eight libraries (payments, config, ai, geo, external, reports, audience and workflow) are marked incubating and sit outside that promise for now. The stability page says why, and what promotes them.
That said, it’s a solo project. Bus factor: one. The community starts now. And if you’re building something you’ll throw away in six months, Wagoe is more than you need.
If you build business software in Clojure, I’d love to hear what works, what doesn’t and what’s missing. Open an issue, or find me on Clojurians Slack.
Thijs Creemers has been building backend systems for 20+ years. Wagoe (pronounced "wah-goo", Southern Dutch for "that’s awesome") came out of building one B2B system in Clojure too many from scratch.