Leaving Wagoe
The example is the product module in examples/shop,
which CI holds equal to what wagoe new and bb scaffold generate. Line counts
and requires below are from that module.
Per layer
| File | Lines | Wagoe in it | Verdict |
|---|---|---|---|
|
68 |
None. Requires only |
Keep |
|
51 |
|
Keep |
|
53 |
None. Malli. |
Keep |
|
55 |
None. Two |
Keep |
|
62 |
|
Keep. One require changes; see Persistence: one require |
|
77 |
None. Ports, core, |
Keep |
|
159 |
No Wagoe require, but two quoted interceptor symbols from |
Rewrite those parts; the handlers stay. See Routes and wiring: the real cost |
|
43 |
|
As |
|
97 |
Integrant, which is not Wagoe, on |
Rewrite, about 30 lines |
|
16 |
None. Plain SQL in Migratus’s file format. |
Keep |
bb check:fcis lets core/ require pure wagoe.* namespaces, as core/ui.clj
does. It guarantees no I/O, not no Wagoe.
Persistence: one require
The generated persistence makes four calls. On next.jdbc and HoneySQL, neither of which is Wagoe, they are one namespace you write once for every module:
(ns shop.db
(:require [honey.sql :as sql]
[next.jdbc :as jdbc]
[next.jdbc.date-time]
[next.jdbc.result-set :as rs]))
(def ^:private ^:dynamic *tx* nil)
(def ^:private opts {:builder-fn rs/as-unqualified-kebab-maps})
(defn- conn [{:keys [datasource]}] (or *tx* datasource))
(defn execute-query! [ctx q] (jdbc/execute! (conn ctx) (sql/format q) opts))
(defn execute-one! [ctx q] (jdbc/execute-one! (conn ctx) (sql/format q) opts))
(defn execute-update! [ctx q] (::jdbc/update-count (jdbc/execute-one! (conn ctx) (sql/format q))))
(defn with-transaction* [ctx f]
(if *tx*
(f *tx*)
(jdbc/with-transaction [tx (conn ctx)]
(binding [*tx* tx] (f tx)))))
The dynamic var matters: the generated transact ignores the connection
with-transaction* passes it, so the repository calls inside it find the
transaction through tx rather than borrowing another connection from the
pool. A nested call joins the outer transaction, as Wagoe’s does; on the same
connection, next.jdbc would commit it early. A Wagoe module that joins the
transaction, such as a workflow start, does not see tx, so move such modules
off together.
Then the module’s diff is this:
(ns shop.product.shell.persistence
"Persistence layer for product module."
(:require [shop.product.ports :as ports]
- [wagoe.platform.database :as db])
+ [shop.db :as db])
(:import [java.time Instant LocalDate]))
The repository still takes a map with :datasource, which is what
:wagoe/db-context is. With this, the generated repository test passes on H2,
as does a test that a throw inside transact rolls the insert back.
What wagoe.platform.database did that this does not:
-
Dialect. It formats with the engine’s HoneySQL dialect. On MySQL, pass
{:dialect :mysql :quoted false}tosql/format: a dialect alone turns on quoting, and a quoted:created-atstayscreated-at. -
Instants. It sends an
Instantparameter as an ISO string, except on MySQL.next.jdbc.date-timesends it as aTimestamp. -
MySQL values. It sends a UUID as text, which Connector/J would otherwise serialise, and reads a
DATETIMEback as the UTCInstantit wrote. -
Constraint violations. A refused unique key, foreign key or CHECK becomes an
ex-infowith:type :conflict, which HTTP answers 409 (platform). Without it theSQLExceptionreaches the client as a 500. -
Redaction. Driver messages that quote the values sent are rewritten before they are logged.
Routes and wiring: the real cost
The cost is in shell/http.clj,
shell/module_wiring.clj, and above all in what the platform assembles around
them.
In shell/http.clj:
-
Authentication. Routes carry
:interceptors, twodef`s naming `wagoe.user.shell.http-interceptors/require-authenticatedandrequire-web-authenticated. Wagoe’s router turns that key into Ring middleware. Plainreitit.ringignores it: the routes keep answering and stop checking who asks. Replace the twodef`s with `:middlewarefrom your own auth. -
Authorization. Requiring sign-in unless a route says
:public true, and:enforceanswering 403 to a route with no rule (Authorization), are platform behaviour, not route data. They go with it. -
Errors. Handlers throw
ex-infowith:type :validation-erroror:not-found, and the platform picks the status (Conventions lists them all). Reitit’s exception middleware dispatches on the same:type:(exception/create-exception-middleware (merge exception/default-handlers {:validation-error (fn [e _] {:status 400 :body (ex-data e)}) :not-found (fn [_ _] {:status 404 :body {:error "not found"}}) :conflict (fn [e _] {:status 409 :body (ex-data e)})})) -
Mounting.
product-routesreturns{:api […] :web […] :static []}, which the platform mounts under/api/v1and/web. Mount them yourself:[["/api/v1" (api-routes svc)] ["/web" (web-routes svc config)]]. -
OpenAPI. The
:openapiroute data is whatreitit.openapireads. It stays.
In shell/module_wiring.clj, Integrant stays. Rename the three :wagoe/product-*
keys, hand the repository {:datasource ds} where it took :ctx, and drop the
:wagoe/product key (Wagoe’s module discovery) and the authorization map
(Wagoe’s permission registry).
Once per application, not per module: src/shop/system_config.clj builds its
Integrant config with wagoe.platform.shell.system.config/system-config. That
call is where Jetty, the Reitit router, the middleware stack (params, Muuntaja,
sessions, CSRF, error pages), the HikariCP pool, Aero config loading and the
migration runner come from. You write that system yourself.
Our estimate, not a measurement: an hour per module shaped like product, and
days for the application system, which is the part Wagoe exists to write.
Modules you replace
| Module | What carries over |
|---|---|
admin |
Nothing to migrate. |
auth ( |
The users, groups and roles tables, created by |
jobs |
With the |
tenancy |
The PostgreSQL schema per tenant stays. You replace the middleware that sets
|
What you lose that is not code
-
The gates.
bb check:fcis,check:ports,check:placeholder-testsand the rest come fromwagoe-tools, a Babashka dependency inbb.edn, not on the application’s classpath (tools). -
The generators.
bb scaffold,bb scaffold integrate,wagoe add. -
The diagnostics.
wagoe doctor,clojure -M:run authz-report,bb create-admin. -
The dev dashboard.
wagoe-devtools, in the:replalias:/dashboard, the route explorer and request inspector (devtools). -
Editor tooling. The Wagoe MCP server in the
:mcpalias (MCP), and the generatedAGENTS.mdthatwagoe agents updatekeeps in line withdeps.edn.