I’ve been building a batteries-included Clojure framework for a couple of years. 1.0 landed end of September, 1.1 three weeks ago. I’d rather use this thread for the argument than the announcement, because the argument is the part I can’t settle on my own.
What it is, briefly
28 libraries across foundation, application and extension tiers, plus five developer tools — 33 artifacts under com.wagoe, all on one version. Every module has the same four-part shape:
{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. Functional Core / Imperative Shell — Bernhardt and Cockburn got there long before I did. The part that’s mine, and the part I’d like picked apart, is that Wagoe doesn’t document that rule. It enforces it. Import a database or logging namespace into core/ and the build fails.
1.1 is the authorization release: permissions, roles and scopes that modules declare, groups hold and routes check. What I spent the time on isn’t the enforcing but the answering — pick a user and a permission and the admin says yes or no and which group made it so; change a group and you see who gains and loses what before anything is written. New apps enforce from the first run, existing apps stay :off until you flip it.
Because the module shape is predictable, scaffolding from a sentence works well enough to put on stage: bb scaffold ai "invoice module: number, customer, line items, total in cents, due date, status draft→sent→paid"produces the schema, the pure core, a ports protocol, an adapter, handlers and tests, wired into Integrant. I put it this way at the Conj: the agent didn’t get smarter, the framework stopped asking it to guess.
The objection I want to hear properly
The strongest case against this is one I find genuinely hard to answer.
Clojure’s ecosystem works because nobody owns the seams. You compose what you need, you swap a piece when it stops fitting, and a bad decision stays local. A framework moves those decisions up a level and makes them mine. When my abstraction is wrong for your case you don’t swap a library — you fight the thing that generated your module. Twenty-eight libraries on one version number is twenty-eight places where that can happen at once.
My answer is that the real choice isn’t framework vs. no framework but explicit conventions vs. implicit ones: every serious Clojure backend already has conventions, usually undocumented, usually living in one or two people’s heads. But that’s a comfortable answer coming from the person who spent two years building the framework, so I’d rather hear where it breaks than defend it.
What I’d actually like to know
Three questions, ordered by how much your answer would change what I do next.
- If you have a Clojure codebase where the architecture lives in one or two people’s heads: would an enforced
core/shellsplit have saved you wiring, or added ceremony around problems you didn’t have? - Where does the enforcement become wrong rather than merely annoying? I know of one — a core function that legitimately wants to log. I’d like to know what else I’m going to hit.
- If you’ve built on Kit, Luminus, Duct or Biff and moved off: what made you leave? I’d rather learn that from you than from my own guesses.
The honest part
Solo project. Bus factor one. The community starts now.
Eight of the 28 libraries — config, ai, geo, external, reports, audience, webhooks and workflow — are marked incubating and sit outside the SemVer promise, with the reason per library on the stability page. payments graduated in 1.1; webhooks is new and took its place.
And the thing I’d rather tell you than have you find: 1.1.0, the authorization release, shipped with an access-control bug in its own admin. List pages let you filter and sort on fields the entity hides, so someone allowed to read users could recover password hashes and MFA secrets a character at a time. Fixed the next day in 1.1.1 — advisory GHSA-g896-jm3g-34h6. CI now opens every page of a freshly generated app on SQLite, PostgreSQL and MySQL, so that class fails the build. I’d rather you weigh the framework knowing that happened.
If you’re building something you’ll throw away in six months, Wagoe is more than you need.
Links
- What it is and why: Wagoe 1.0: ship your Clojure app, not the plumbing around it — Wagoe Blog
- 1.1.1 release notes: Release: v1.1.1 — a security fix, and SQLite that works — Wagoe Blog
- Stability and tiers: Stability & Versioning — Wagoe Docs
- Roadmap, one claim per release: Roadmap — Wagoe Docs
- Source and samples: GitHub - wagoebv/wagoe: Batteries-included Clojure web framework built on Functional Core / Imperative Shell (FC/IS) — JWT+MFA auth, auto-generated admin UI, background jobs, multi-tenancy, payments, and AI-native scaffolding. · GitHub
curl -fsSL ``https://get.wagoe.org`` | bash, thenwagoe new my-app