A development toolbar inspired by Django's debug-toolbar. Appears when
:dev? is true in config.edn, backed by middleware that is only loaded
when the dev alias is active.
Fixed bar at the bottom of every page, in normal document flow (not
floating — pushes footer upward). Collapsed by default to a thin ~28px
strip, expandable via CSS checkbox hack (hidden <input type="checkbox">
Collapsed view: "Dev Toolbar" label + summary metrics (42ms | 7 SQL 12ms).
Expanded view (~120px):
| Left (metrics) | Right (actions) |
|---|---|
Request time: 42ms |
Take Snapshot (text input for optional name, defaults to timestamp) |
SQL: 7 queries, 12ms |
Restore Snapshot (dropdown of existing snapshots, defaults to most recent) |
| Route-specific actions |
Route-specific actions:
/documents/:id → "Delete Document" button (cascading delete from DB + S3)Styling: Inline styles in Hiccup (no production CSS changes). Colors from
existing theme: #0d1117 background, #30363d border, #10b981 accent.
Monospace font for metric values.
A single middleware (wrap-dev-toolbar) does everything:
System/nanoTime start.next.jdbc/with-logging — the sql-logger returns
(System/nanoTime) as state, the result-logger computes elapsed ms and
swap!s query info into the atom.:db-pool in the
injected config).:body is a
hiccup.util.RawString.(str (hiccup/html ...)).</body> with
toolbar-html</body>. Return the modified response with a plain String
body.RawString (skip HTMX fragments, JSON,
redirects, etc.).This is the same approach Django's debug-toolbar uses: render after the handler completes, string-replace into the response.
Dev-only routes (registered conditionally):
POST /dev/snapshot — take a new timestamped snapshotPOST /dev/snapshot/:timestamp/restore — restore a specific snapshotDELETE /dev/documents/:id — delete document with cascadedev/snapshots/
2026-03-21T14-30-00/
snapshot.edn
s3/
documents/...
ingestions/...
2026-03-21T16-45-12/
snapshot.edn
s3/
Each snapshot is a timestamped directory, never overwritten.
dev/snapshots/ is gitignored.
Single snapshot.edn file containing a map keyed by table name, each value
a vector of row maps:
{:document [{...} {...}]
:ingestion [{...} {...}]
:document_match [{...} {...}]
...}
Tables (9, in dependency order for inserts):
document_clusterdocumentingestioningestion_post_process_statdocument_matchsupplier_verificationsupplier_verification_documentqa_dataset_itemdatev_export_auditEDN serialization: Custom readers/writers for java.util.UUID,
java.time.Instant, java.time.LocalDate, and JSONB (PGobject) — same
approach as the existing fetch-document babashka tool.
Column compatibility on restore: Read current columns from
information_schema.columns for each table. For each row in the snapshot,
keep only keys that exist in the current schema. Missing (new) columns get
database defaults. Dropped columns are silently skipped.
Pure AWS SDK v2 implementation — list-objects-v2, get-object, put-object!
— self-contained in the dev namespace, not in aws.clj.
Snapshot: List all objects in the storage bucket, download each to
dev/snapshots/<timestamp>/s3/ preserving key paths.
Restore: Delete all objects in the bucket, then upload everything from
dev/snapshots/<timestamp>/s3/ back.
Cascade delete from the 9 tables in reverse dependency order (by
document_id or related ingestion_id), then delete S3 objects:
documents/<id>.*, documents/<id>/, and ingestions/<ingestion-id>/ for
each related ingestion.
All code lives in dev/ (only on classpath with :dev alias):
dev/
user.clj # existing
com/getorcha/dev/
toolbar.clj # middleware, routes, rendering
toolbar/
snapshot.clj # snapshot/restore/delete logic
In app/http.clj, routes and middleware are loaded via requiring-resolve
wrapped in try/catch:
(let [dev-routes (when dev?
(try ((requiring-resolve 'com.getorcha.dev.toolbar/routes) config)
(catch Exception _ nil)))
dev-middleware (when dev?
(try (requiring-resolve 'com.getorcha.dev.toolbar/wrap-dev-toolbar)
(catch Exception _ nil)))]
...)
If dev/ is not on the classpath (e.g., sandbox running from uberjar without
extra paths), the requiring-resolve fails silently and the toolbar is not
loaded.
scripts/ci/build.clj accepts an optional :extra-paths CLI argument:
(defn uber
[{:keys [extra-paths]}]
(let [basis (c.t.build.api/create-basis {:project "deps.edn"})
src-dirs (into (:paths basis) extra-paths)
...]
...))
Tolaria demo builds with:
clojure -X:build :extra-paths '["dev"]'
Default clojure -X:build (no args) is unchanged.