Date: 2026-04-19
Status: Draft
Author: Thijs Creemers + Claude
Parent spec: docs/superpowers/specs/2026-04-18-mindblowing-dx-design.md (Phase 5 section)
Phases 1-4 built the devtools foundation: error pipeline (classify → enrich → format → fix), REPL helpers (routes, simulate, query, schema, trace, fix!), guidance engine, and a 7-page dev dashboard. Phase 5 adds advanced REPL features that make the REPL a full development cockpit — time-travel debugging, runtime route injection, request tapping, component hot-swap, and schema-driven module generation.
All 6 features from the parent spec's Phase 5:
(recording) — time-travel debugging (start/stop/replay/diff/save/load)(prototype!) — schema-driven full-module generation(restart-component) — Integrant component hot-swap(defroute!) — runtime route addition(tap-handler!) / (untap-handler!) — request interception(scaffold!) — REPL wrapper around scaffolder| Decision | Choice | Rationale |
|---|---|---|
| Recording persistence | File-based (.boundary/recordings/*.edn) | Survives restarts, barely more complex than in-memory |
prototype! generation | Delegate to libs/scaffolder/ core | Scaffolder core is pure Clojure, JVM-compatible, avoids duplication |
defroute! injection | Rebuild Reitit router | Dynamic routes get full interceptor support as first-class citizens |
tap-handler! strategy | Interceptor injection via router rebuild | Shares router rebuild infra, gives access to full interceptor context |
| Handler swapping | Atom-based handler wrapper in platform | Jetty holds a direct function reference; atom indirection enables runtime swap without server restart |
restart-component config | Re-resolve from Integrant config map | Picks up config changes; consistent with (reset) behavior |
The platform HTTP server (libs/platform/) currently passes the compiled handler directly to Jetty as a closure. Jetty holds a direct function reference — there is no indirection layer. To support runtime router rebuilds, we introduce an atom-based handler wrapper:
;; In libs/platform/src/boundary/platform/shell/system/wiring.clj
(defonce ^:private handler-atom (atom nil))
(defn dispatch-handler [request]
(@handler-atom request))
;; At init: (reset! handler-atom compiled-handler)
;; Jetty receives dispatch-handler (stable reference)
;; Router rebuilds swap handler-atom (no server restart)
This is a small, surgical change to wiring.clj:
handler-atom (defonce)dispatch-handler to Jetty instead of the compiled handler directlyswap-handler! function that devtools can callThe atom is only used in dev profile. In production, the handler is passed directly (no indirection overhead).
The dashboard's wrap-request-capture middleware (Phase 4) captures sanitized request summaries for the request inspector page. Recording needs full request/response bodies for faithful replay. Rather than modifying the existing capture:
They serve different purposes and operate independently.
libs/devtools/src/boundary/devtools/
├── core/
│ ├── recording.clj # Pure: session data, filtering, diffing, serialization
│ ├── router.clj # Pure: route tree manipulation, interceptor injection
│ └── prototype.clj # Pure: build scaffolder context from prototype spec
└── shell/
├── recording.clj # Stateful: atom management, file I/O, middleware install
├── router.clj # Stateful: router atom swap, dynamic route/tap tracking
└── prototype.clj # Effectful: file writes, migration, system reset
shell/repl.clj — add restart-component, scaffold! functionsdev/repl/user.clj — expose new helpers to REPL namespacelibs/platform/src/boundary/platform/shell/system/wiring.clj — handler-atom wrapper (dev profile only).gitignore — add .boundary/recordings/defroute!, tap-handler!, and recording all need to rebuild the Reitit router. A shared shell/router.clj provides:
rebuild-router! — takes current route tree, applies modifications, compiles + swapsdynamic-routes atom — tracks routes added via defroute!taps atom — tracks active handler taps(reset), all dynamic modifications clear — these are ephemeral dev aidsPlatform integration: Platform stores the handler as a direct closure passed to Jetty. The handler-atom wrapper (see Architecture section above) enables runtime swapping. shell/router.clj calls swap-handler! after recompiling the router.
Data model:
{:id "auth-flow"
:entries [{:idx 0
:request {:method :post :uri "/api/users" :body {...} :headers {...}}
:response {:status 201 :body {...} :headers {...}}
:duration-ms 42
:timestamp #inst "..."}
...]
:started-at #inst "..."
:stopped-at #inst "..."}
API:
(recording :start) ; Install capture middleware, start session
(recording :stop) ; Remove middleware, freeze session
(recording :list) ; Print captured requests as table
(recording :replay 3) ; Replay entry #3 via simulate
(recording :replay 3 {:email "x"}) ; Replay with modified body (deep-merge)
(recording :diff 3 5) ; Diff two entries (colored add/remove)
(recording :save "auth-flow") ; Write to .boundary/recordings/auth-flow.edn
(recording :load "auth-flow") ; Read back, set as active session
Implementation:
(recording :start) installs a Ring middleware via router rebuild that captures full request/response pairs into a session atom(recording :stop) removes the middleware, freezes the session(recording :replay N) takes entry N and runs it through the existing simulate function in repl.clj(recording :diff M N) uses a pure data diff on both request and response maps, formatted with colored additions/removalspr-str / edn/read-string to .boundary/recordings/Core layer (core/recording.clj):
create-session — empty session with timestampadd-entry — append captured request/response to sessionget-entry — retrieve by index with bounds checkmerge-request-modifications — deep-merge user overrides into a captured requestdiff-entries — produce a structured diff of two entriesformat-entry-table — format entries as printable tableserialize-session / deserialize-session — EDN round-tripShell layer (shell/recording.clj):
active-session atom — current recording sessioncapture-middleware — Ring middleware that captures into the atomstart-recording! — installs middleware via router/rebuild-router!stop-recording! — removes middleware, freezes sessionreplay-entry! — delegates to repl/simulate-requestsave-session! / load-session! — file I/O to .boundary/recordings/Error handling:
(recording :replay N) with no active session: prints "No active recording session. Use (recording :start) or (recording :load "name")."(recording :replay N) with out-of-bounds index: prints "Entry N not found. Session has M entries (0 to M-1)."(recording :save "name") with no active session: prints "No active recording session."(recording :load "name") with missing file: prints "Recording 'name' not found. Available: ..." (lists .boundary/recordings/ contents).Core layer (core/router.clj):
add-route — merge a route definition [method path handler-map] into a route treeremove-route — remove by method + pathinject-tap-interceptor — add a :devtools/tap interceptor to a handler's chain (by handler keyword)remove-tap-interceptor — remove itinject-capture-interceptor — add recording capture interceptorAll pure data transformations on Reitit route data structures.
Shell layer (shell/router.clj):
system-router-ref — gets the router reference from the running Integrant systemdynamic-routes atom — {[:get "/api/test"] handler-map}taps atom — {:create-user callback-fn}rebuild-router! — applies dynamic-routes + taps + recording middleware to the base route tree, compiles new Reitit router, swaps atomclear-dynamic-state! — called on (reset), clears atoms(defroute!)(defroute! :get "/api/test" (fn [req] {:status 200 :body {:hello "world"}}))
(remove-route! :get "/api/test")
(dynamic-routes) ; List injected routes
dynamic-routes atomrebuild-router!(dynamic-routes) lists all currently injected routes(tap-handler!) / (untap-handler!)(tap-handler! :create-user (fn [ctx] (println "Request:" (:request ctx)) ctx))
(untap-handler! :create-user)
(taps) ; List active taps
taps atomrebuild-router! to inject a :devtools/tap interceptor at the start of the handler's interceptor chain:enter, passing the full interceptor context(taps) lists active taps(restart-component)(restart-component :boundary/http-server)
ig/halt-key! on the component(reset) behavior)ig/init-key with the resolved config value(reset).Intentionally simple — thin wrapper around Integrant.
(scaffold!)(scaffold! "invoice" {:fields {:customer [:string {:min 1}]
:amount [:decimal {:min 0}]}})
boundary.scaffolder.core.generators functions directly (JVM-compatible)libs/<name>/bb scaffold integrate)core/guidance.clj(prototype!)(prototype! :invoice
{:fields {:customer [:string {:min 1}]
:amount [:decimal {:min 0}]
:status [:enum [:draft :sent :paid]]
:due-date :date}
:endpoints [:crud :list :search]})
Higher-level than scaffold! — the "zero to working module" experience:
scaffold! for file generationgenerate-migration-file(reset) to load the new module(simulate) commandCore layer (core/prototype.clj):
build-scaffold-context — maps prototype spec to scaffolder template contextendpoints-to-generators — maps :endpoints keywords (:crud, :list, :search) to generator function callsbuild-migration-spec — converts field spec to migration column definitionsShell layer (shell/prototype.clj):
prototype! — orchestrates: generate → migrate → reset → print summaryNew functions exposed in dev/repl/user.clj:
;; Recording
(recording :start)
(recording :stop)
(recording :list)
(recording :replay N)
(recording :replay N overrides)
(recording :diff M N)
(recording :save "name")
(recording :load "name")
;; Dynamic routing
(defroute! method path handler-fn)
(remove-route! method path)
(dynamic-routes)
;; Handler tapping
(tap-handler! handler-kw callback-fn)
(untap-handler! handler-kw)
(taps)
;; Component management
(restart-component key)
;; Module generation
(scaffold! name opts)
(prototype! name spec)
core/recording_test.clj — session creation, entry add/get, diff, serialization round-trip, merge modificationscore/router_test.clj — add/remove route from route tree, inject/remove tap interceptorcore/prototype_test.clj — context building, endpoint mapping, migration spec generationshell/recording_test.clj — start/stop recording, capture middleware integration, save/load file round-tripshell/router_test.clj — router rebuild with dynamic routes, verify routes are reachableshell/prototype_test.clj — full prototype! flow generates expected file structure(recording :start) / (recording :stop) captures requests; (recording :replay N) reproduces them(prototype!) generates a working module that passes tests after (reset)libs/scaffolder/ core — JVM-compatible pure functions. Specifically: boundary.scaffolder.core.generators (generate-schema-file, generate-ports-file, generate-core-file, generate-migration-file, generate-service-file, generate-persistence-file, generate-http-file) and boundary.scaffolder.core.template (field/entity context builders). libs/scaffolder must be on the :dev classpath in deps.edn.ig/halt-key!, ig/init-key) for restart-componentlibs/platform/ — small change to shell/system/wiring.clj for handler-atom wrapper (see Architecture section)Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |