All notable changes to the Wagoe Framework will be documented in this file.
Note: entries below the "Renamed" section predate the Boundary → Wagoe rename and keep their original
boundary-*names,:boundary/*keys, andorg.boundary-appcoordinates on purpose — they describe releases that shipped under those names.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning
from 1.0.0 onwards. Until then, breaking changes are permitted between beta
releases — see the Stability & Versioning policy
for what is public API, what is internal, and how deprecations are announced.
Note — the version scheme changed, and the number went down. Releases were
1.0.1-alpha-Nup to1.0.1-alpha-42(2026-07-19); they are1.0.0-beta-Nfrom1.0.0-beta-1(2026-07-23). The old scheme read as a patch release of a shipped1.0, which had never happened.Because Maven sorts
1.0.0below1.0.1, every beta compares as older than the last alpha. Anything resolving "newest" will pick1.0.1-alpha-42over the current release — pin exact versions. The1.0.1-alpha-*line is discontinued and receives no fixes.There is no separate
1.0.0-beta-1entry below. Its changes were still in[Unreleased]when1.0.0-beta-2shipped eight days later, so they are recorded under the1.0.0-beta-2heading.
Running as more than one process. 1.0.0-beta-4 could serve an application from
a single JVM and little else: cross-module calls had a protocol seam but nothing
that crossed a network, the prod profile could not boot, and the deployment
documentation described topologies with no reference to run them from.
This release makes the seam real. A module can be served over HTTP and called through the protocol its callers already use, one or several modules can be booted as their own service, and modules can tell each other things asynchronously through a new event bus. A circuit breaker keeps a service that is down from taking its callers with it, and the state behind all of it lives where every replica can see it rather than in one JVM's memory.
The other half is what looking closely turned up. Holding the cache and
job-queue adapters to a shared contract found twenty-one places where they
disagreed — including three different answers to what order jobs come off a
queue in, two of them newest-first. A failed production boot logged the database
password. install.sh accepted a JDK too old to run any of this. Those were all
shipped behaviour, and none of it was visible from the suite that was supposed
to be watching.
Three HikariCP pool keys that no build has ever applied (BOU-89).
:keepalive-time-ms, :validation-timeout-ms and
:leak-detection-threshold-ms were documented and accepted by nothing: the
pool map is a :closed Malli schema and the builder applies five keys, so a
config setting any of them failed the boot at :wagoe/db-context. Removed
rather than implemented — the prod and acc profiles shipped with them and
could not start.
Integrant config for four libraries that register none (BOU-284,
BOU-286). wagoe add jobs|calendar|reports|ui-style wrote
:wagoe/<lib> {:provider :in-memory} into generated projects, and nothing
reads it; ui-style's own AGENTS.md says it has no Integrant keys.
:post-install now says how each library is actually used. wagoe add push
wrote one key where the library registers seven :wagoe.push/*, so the
generated config.clj assembles them properly instead.
bb scaffold new / wagoe scaffolder new (BOU-259). Projects were
generated by two independent implementations: the wagoe new templates in
libs/wagoe-cli, and a second copy inside the scaffolder. The copy had
drifted until it no longer produced a Wagoe project — 7 files against the
CLI's 20, with no com.wagoe dependencies, no main.clj/system.clj, no
build.clj, no tests.edn and no .env, so the result could not boot, test
or build. Both commands now print the replacement (wagoe new my-app) rather
than failing as an unknown command, and both exit non-zero so a script that
still calls them cannot read the redirect as a generated project — including
--help, which the scaffolder CLI briefly answered with root help and exit 0,
making the removed command look available to anything probing for it.
Supersedes ADR-002.
A module can run in another process without its callers knowing (BOU-90).
wagoe.platform.shell.rpc serves any module's protocol over HTTP, and
remote-adapter returns a value implementing that same protocol by calling
it — so a call site keeps using the port it already used. transit+json on the
wire (JSON has no keywords), a required x-rpc-service-key compared in
constant time, and the server resolves an operation against the protocol's
own :sigs, so the endpoint cannot become a general-purpose remote eval.
service launch mode (BOU-91). java -jar wagoe.jar service payments
boots only the modules named plus the platform they need; several can share
one process. Declared in config.edn, on its own listener. The counterpart
to the remote-port adapter: one of them slices a module out, the other calls
it.
Reference deployment topologies (BOU-89).
deploy/compose/multi-instance.yml (N replicas behind nginx, Redis-backed
cache, sessions and rate limiting), deploy/compose/per-service.yml (one
module as its own service), deploy/k8s/wagoe.yaml, and
docs/modules/architecture/pages/deployment-topologies.adoc describing when
each applies.
libs/events — an asynchronous event bus (BOU-93). The other half of the
cross-process story: a publisher does not know who is listening, does not
wait, and is unaffected if a consumer is down. Two adapters — in-memory, and
Redis Streams with consumer groups, at-least-once delivery, reclaim of
abandoned entries and a dead-letter stream after :max-deliveries. Three
protocols rather than one, so a module that only emits does not depend on
subscription machinery it never calls.
A circuit breaker for the remote-port adapter (BOU-285). Retries bound
the damage of one call; this bounds the damage of many. State lives in the
cache port, so replicas share one breaker rather than each discovering the
outage separately, and a set-if-absent! lease lets exactly one replica
probe when the window elapses instead of all of them. Trips on consecutive
:rpc/unavailable and :rpc/timeout — failures where the call did not reach
the service — and returns :rpc/circuit-open with :retry-after-ms, which a
log can tell apart from "tried and could not reach it". Opt-in: without a
:cache there is no breaker and the client behaves as before.
wagoe new --no-user (BOU-234). The scaffold wired the user chain
unconditionally, so every generated application carried authentication and
four tables whether or not it had accounts. Platform has been decoupled from
user since BOU-171; this makes the scaffold's default a choice.
Realtime topics accept in-process subscribers (BOU-233). Every subscriber had to be a WebSocket connection, so an application wanting an event to reach server-side code ran a second pub/sub beside this one.
Four dev-workflow Claude Code skills (BOU-235) — wagoe-doctor,
wagoe-migrate, wagoe-scaffold and wagoe-debug, beside the existing
wagoe and wagoe-setup. Closes BOU-237, BOU-238, BOU-240 and BOU-242.
Adapter contract suites for cache and jobs (BOU-288, BOU-289). Each
library's adapters had separate test suites sharing no cases, so nothing said
they behave alike — and they did not. One sweep per library now runs the port
against every adapter; between them they found twenty-one divergences that
the per-adapter suites had been passing over. libs/events gained the same
thing with BOU-93.
bb check:changelog. A branch that changes shipped src/ must add an
entry here. Thirty pull requests merged in the eleven days to 2026-08-16
without one between them — a new library, a new launch mode, a removed config
key and a change to the order jobs are dispatched in. Nothing checked. Tests,
docs, CI and dev tooling are out of scope, and [no changelog] in a commit
message waives it for a source change nobody will notice.
Gates. Required status checks are verified against the job names CI emits
(BOU-277) with no repository secret; every third-party namespace a library
requires must be declared, and the allowlist is empty (BOU-273, BOU-276); a
library with a build.clj must have a documentation page (BOU-93).
Three more first-run matrix cells (BOU-232). A machine with a JDK too old
to use and a machine with no network, both in first-run-preconditions.sh;
and zsh alongside bash and fish in the adversarial suite, which had been
verified by hand once and never since. The old-JDK cell found the install.sh
defect below. The offline cell pins behaviour that was already correct: the
installer fails with something actionable rather than raw curl output.
Nightly first-run matrix (BOU-232). The broad first-run coverage —
Ubuntu, Fedora and Arch for the install-to-serving-app path, plus the
adversarial cases on Ubuntu and Fedora — now runs on a schedule and on
demand, rather than only when someone remembers. The fast single-image smoke
test stays on every push. workflow_dispatch makes the same matrix the
pre-release gate.
Job dispatch is FIFO within a priority, on every backend (BOU-289).
Behaviour change. The three IJobQueue backends had three different
answers: the DB adapter was FIFO, the in-memory adapter ran critical, high
and normal newest-first, and Redis ran :low newest-first. Anything relying
on the old dispatch order in development or on Redis :low will see a
different order. The DB adapter's ORDER BY priority_rank, created_at is now
what all three do.
The cache adapters agree about expiry, batch reads and patterns
(BOU-288). Behaviour change. An expired key now reads as absent from every
operation rather than only from get-value: delete-key! and expire!
return false for one, keys-matching omits it, and compare-and-swap!
matches nil against it. ttl rounds up, as Redis does, instead of
reporting 29 for a key set to 30 a millisecond earlier. get-many returns
keys holding false or nil instead of dropping them. compare-and-swap!
on Redis keeps the key's TTL. And the in-memory pattern matcher treats
everything but *, ? and […] literally — it compiled the glob straight
to a regex, so a.b matched axb.
Heavy test dependencies moved out of the shared :test alias (BOU-260).
Embedded PostgreSQL's two platform binaries, the OpenTelemetry in-memory
exporters and clj-http-lite sat in :test, which all 27 per-library CI jobs
resolve, so one flaky artifact failed them all under an innocent library's
name. They now live in :test/pg, :test/pg-mac, :test/otel and
:test/http; :test/all composes them for a full local run.
platform no longer requires any module's wiring (BOU-131). The system
wiring statically required ten module-wiring namespaces, so every consumer of
platform had to ship every one of those jars whether it used them or not, and
a missing one was a FileNotFoundException at load. The layer that emits a
key now registers it.
install.sh accepted any JDK, including ones too old to run Wagoe
(BOU-232). The check was java -version | grep -q "version", which every JDK
back to 8 passes, while the installer's own text says JDK 21+. On a machine
with an older JDK it reported "JVM already installed" and carried on, and the
failure surfaced much later as a class-file-version error out of the Clojure
compiler — which tells a newcomer nothing. It now reads the major version,
says which one it found and which is needed, installs a current JDK, and
verifies the result rather than assuming it: an older JDK still first on PATH
is a loud failure naming the one that is winning, not a silent success.
A deleted job stayed on the queue (BOU-289). Redis and the in-memory
backend removed the job data but left its id in the list, so it still counted
towards queue-size and the dequeue that reached it found nothing and
returned nil — work queued behind a deleted job waited for a poll that might
not come. peek-job on Redis read only the :normal list, so a queue holding
a critical job peeked as empty and disagreed with the very next dequeue.
list-queues returned a Redis queue once per priority in use, and named
in-memory queues that had been fully drained.
The in-memory cache lost concurrent writes while reclaiming expired
entries (BOU-288). delete-key! and expire! read the entry and then wrote
based on what the read said, so a value written in between was deleted while
the caller was told there had been nothing there; expire! could also
recreate a key deleted since the read as an entry with an expiry and no value,
which exists? reported as present and get-value threw on. Both are one
swap-vals! now.
A BigInteger beyond 64 bits was silently lost by the Redis cache
(BOU-288). It took the native-integer path, where reading it back overflows
Long/parseLong and the decimal bytes are not Nippy either — so the write
reported success and the read reported a miss. get-many and delete-many!
also handed an empty collection straight to MGET and DEL, which is an error
rather than an empty answer.
The prod and acc profiles could not boot (BOU-89). :port came from
#env POSTGRES_PORT as a string against a [:port pos-int?] schema, on top
of the three pool keys above.
The documented way to run wagoe-mcp corrupts the protocol (BOU-105).
Fixed along with the docs that described it.
The AI CLI discarded unknown options and swallowed the failures it was
built to report (BOU-279, BOU-280). Every subcommand destructured
parse-opts without reading :errors, so a typo'd flag was ignored rather
than rejected; failures named neither the provider that failed nor the
endpoint that was configured, and an exhausted balance was reported as a rate
limit.
bb ai gen-tests emitted tests that did not compile (BOU-239), and
bb i18n:scan — a required CI job — could not report anything (BOU-241).
The documented ways to run the app now run the app (BOU-243).
clojure -M:repl-clj could not start the system.
bb migrate create wrote to a shadowed directory (BOU-274). Migrations
under a migrations/ that lost the classloader race were skipped silently;
it now fails loudly. bb scaffold field listed a file it never opened
(BOU-275).
Five quality items, each with a gate behind it (BOU-92, BOU-151, BOU-61, BOU-253, BOU-245). The dependency allowlist is empty; platform no longer makes the SMTP/IMAP/Twilio adapters a mandatory dependency of the HTTP layer.
The scaling and deployment documentation described a system from several
tickets ago. Two entries told a reader to do something that breaks, and the
rest was drift — launch modes documented as absent, shipped adapters
described as unbuilt, and libs/events missing from the readiness matrix.
Every bb ai subcommand failed in a generated project (BOU-272).
wagoe.tools.ai shelled a plain clojure -M -m wagoe.ai.shell.cli-entry,
but generated projects carry com.wagoe/wagoe-ai only in their :mcp alias,
never in :deps — so explain, gen-tests, sql, docs and
admin-entity all died with a FileNotFoundException. All five are listed in
the generated bb.edn, the generated AGENTS.md, and the shipped wagoe
Claude Code skill. The dependency is now injected via -Sdeps, matching what
bb scaffold has always done, with a WAGOE_AI_ROOT override for exercising
unreleased AI code from a generated project.
bb migrate create threw a ClassCastException (BOU-271). The migration
config carries :migration-dir as the discovered vector of every directory
on the classpath; up, status and rollback accept that, but
migratus/create casts it to String. So the documented way to add a
migration failed for everyone, pushing people onto hand-written files — the
exact path BOU-256 was filed against, because that filename format is easy to
get wrong and silently invisible to migratus. Creation now receives the
project's own migrations/ as a string.
bb check's Config doctor gate could never fail (BOU-270). It invoked
bb doctor without --ci, and doctor prints its errors but exits 0 unless
that flag is set — so the row reported ✓ for every config, including one
that did not parse. It now passes --ci. Every other checker in the registry
already exits non-zero on violations; doctor was the only flag-gated one.
Scaffolded modules now pass bb check (BOU-267). Generated source
produced 36 clj-kondo warnings, and clj-kondo exits non-zero on warnings, so
bb check failed the moment a user scaffolded their first module. Two of
those were real defects rather than lint noise: update-<entity> was
declared in both the repository and service protocols in one namespace, so
the second silently overwrote the first and ports/update-<entity> carried
the wrong arity; and the generated service called .list-<plural> on its
repository, which only declares find-all, so listing failed at runtime. The
repository method is now update-entity, the service calls find-all, and
the remaining warnings — unused this/req/config bindings, an unused
require, a partially-reified protocol — are gone. bb check on a freshly
scaffolded module is 9/9.
wagoe new into a directory you cannot write surfaced a stack trace
instead of a permissions message (BOU-232). The pre-flight directory check
cannot catch it — the target does not exist yet, so the failure comes out of
clojure.java.io/writer partway through generating. Found by the adversarial
suite's read-only case, which had never actually run: it skipped whenever the
container was root, because chmod does not restrict uid 0. It now uses a
read-only bind mount, which the kernel enforces for every uid, so the whole
suite runs with nothing skipped for the first time.
bb check reported failures a user could not act on (BOU-264). It runs
each check as a subprocess (bb check:fcis, …), but five of them only mean
something in the Wagoe repository — doc-counts and poms compare against
the published library set, agents diffs knowledge.edn, no-boundary is a
rename gate for this repo's history, and docs:lint lives on the dev/ path.
Generated projects define none of those tasks, so bb exited 1 on "File does
not exist" and they were reported as violations. Checks now declare a scope
and the framework-only ones are skipped outside this repo — and named in
the output, because a silently shorter list reads as a clean run. Generated
projects also gain check:test-meta, check:test-tags and check:hygiene,
which were monorepo-only despite being useful anywhere.
bb create-admin could not create a user at all (BOU-266). The
:user-cli alias ran the CLI through -e reading *command-line-args*, and
clojure.main takes the first non-option argument as a script path — so the
create verb was dropped and the CLI rejected --email as an unknown global
option. Without an admin user the admin UI redirects to a login nobody can
pass. The alias now uses -m against a new -main on
wagoe.user.shell.cli-entry. Existing generated projects pick this up when
they move to a release containing it.
Generated projects were missing the check:ports bb task while bb check
shelled out to it, so the hexagonal gate BOU-80 requires — and that the
generated AGENTS.md documents — could not run in a new project. Found
while reconciling the two generators.
H2 is now file-backed in dev (BOU-265). bb setup --database h2 wrote
:memory true for every environment, and an in-memory H2 database is private
to the JVM that opened it. bb migrate up, bb create-admin and the app are
three separate processes, so each got its own empty database: migrations
applied nowhere, the admin user was written nowhere, and the app booted
unmigrated — with every step exiting 0. bb quickstart --preset minimal, the
first-listed preset, could not produce a working app. Dev and other non-test
environments now use ./<env>-h2-database; the test profile keeps in-memory
H2, which is correct for a single JVM. The path is explicitly relative
because H2 2.x rejects an implicitly-relative one.
The root README said a new project gets "H2 in-memory database (zero-config)". It gets SQLite, and has since before the rename.
A failed production boot no longer logs the database password (BOU-244).
wagoe.main logged the exception on any startup failure, and Integrant's
ex-data carries :value — the config map for the key that failed, which for
:wagoe/db-context is the database configuration.
js-yaml and brace-expansion advisories cleared in the docs build (GHSA-5p4m-2wfm-xmqj, GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895). Both are dev-only transitive dependencies of Antora, which parses only our own playbook, so the exposure was theoretical; the override pinning js-yaml did not exclude the affected release.
Everything a new project touches. 1.0.0-beta-3 shipped a scaffolder whose
migrations were never applied, so the sample module bb quickstart creates had
no database table — and reported success while doing it.
001_create_tasks.sql; migratus discovers <id>-<name>.up.sql, so the file
sat on disk and bb migrate status reported "0 pending" while
bb quickstart reported 8/8 Done — running zero migrations succeeds. Also
fixes the id: the counter scanned resources/migrations while writing to
migrations/ and parsed ids with Integer/parseInt, which overflows on
14-digit timestamps, so every module got 001. Ids are now UTC timestamps
that step forward on collision, and each migration gets a matching
.down.sql. wagoe scaffold field had the same defect.bb doctor no longer passes configs the application cannot load. An
unparseable config.edn, or :active misspelled, both left every check
inspecting an empty map and reporting a pass — bb doctor --ci, a CI gate,
exited 0 on a config that fails at runtime with No active database configured.install.sh supports Fedora and the RHEL family. Previously
"Unsupported OS" with no path forward. which also joins the prerequisite
check: babashka's installer calls it and minimal Fedora images do not ship
it, so the run died inside a third-party script./web/admin without the trailing slash, and
wagoe add admin now says that bb create-admin is needed to log in.bb db:seed. Previously advertised in bb.edn and printed "not yet
implemented". Seed files are EDN — a map of table → rows, or a vector of
[table rows] pairs when order matters. Inserts run in one transaction, and
seeding refuses outside development environments unless --force is given.src/<ns>/main.clj,
a :run alias for foreground start, a :build alias producing an uberjar,
and a Dockerfile. Previously a generated project could only be started from
an editor-connected REPL, so it could not be containerised or supervised.
Shutdown is graceful — a container stop drains the server and closes the
pool.wagoe new →
bb quickstart → serving app inside a bare container, asserting on HTTP
rather than exit codes.clojure -M:repl-clj and
export WAG_ENV="development". Neither works in a generated project: the
alias exists only in the monorepo, and there is no conf/development
profile. Corrected across the getting-started, index, repl-workflow and
monorepo pages.The release that makes the documented install path true. 1.0.0-beta-2
advertised a first-run flow that did not work on a machine with nothing
installed; every failure below was measured in a clean ubuntu:24.04
container, not inferred.
org.xerial/sqlite-jdbc
now ships in a generated project's deps.edn, and the generated config reader
gained the :wagoe/sqlite branch it previously lacked.bb setup defaults to SQLite on all three entry points — interactive menu,
bb setup ai, and flag invocations such as bb setup --payment mock.bb quickstart no longer runs the configuration wizard over a config that
wagoe new has just written. Pass --preset <name> to reconfigure deliberately.bb quickstart's banner says it will verify rather than start; it never
started the app, and claiming otherwise sent people hunting for a failure that
had not happened.install.sh failed three separate ways on clean Linux (BOU-226):
curl/git/unzip/zip up front and prints the exact command for
the detected OS.set -euo pipefail aborted with
SDKMAN_CANDIDATES_API: unbound variable immediately after sdkman printed
"All done!". The source and sdk install now run under set +u.sudo was assumed to exist, which it does not in containers or minimal
images. Worse, the steps were written sudo ./installer && rm installer, and
set -e exempts the failure of any command in an && list except the last —
so a failed install fell through and printed ✓ Clojure CLI installed having
installed nothing. Adds as_root() and reachable || fail handling.ClassNotFoundException: org.postgresql.Driver and nothing ever served.:database-path where the platform's config reader
looks for :db, yielding database-path nil and a Malli validation abort at
migration time./admin returns 404 in a new project — com.wagoe/wagoe-admin ships in
deps.edn but :wagoe/admin is not wired into the generated config (BOU-229).:run alias, -main, or :build alias, so the app
can only be started from a REPL via (go) (BOU-254).First release under the Wagoe name, on the com.wagoe Clojars group.
com.wagoe/wagoe-<lib> (previously
org.boundary-app/boundary-<lib>). The group matches wagoe.com; org.wagoe
was never claimable, since Clojars verifies a reverse-domain group against its
matching domain and wagoe.org was not owned at the time.framework.wagoe.com, and subsequently to wagoe.org with the
older hostnames kept as permanent redirects.pom.xml carrying a vulnerable pin was removed.The framework is renamed from Boundary to Wagoe ahead of the first
public release, and versioning moves to plain SemVer starting 1.0.0-beta-1.
This is a hard rename with no compatibility shims: nothing had been published
under a stable version, so no deprecation window is provided.
Migration — mechanical replacements, in this order:
| Kind | Before | After |
|---|---|---|
| Namespaces | boundary.<seg>.… | wagoe.<seg>.… |
| Integrant / config keys | :boundary/http-server | :wagoe/http-server |
| Maven / Clojars coords | org.boundary-app/boundary-<lib> | com.wagoe/wagoe-<lib> |
| deps.edn local aliases | boundary/<lib> | wagoe/<lib> |
| Environment variables | BND_*, BOUNDARY_* | WAG_* |
| CLI binary | boundary <cmd> | wagoe <cmd> |
| Resource paths | boundary/i18n/translations | wagoe/i18n/translations |
| MCP resource URIs | boundary://… | wagoe://… |
MCP server name (.mcp.json) | "boundary" | "wagoe" |
AGENTS.md region markers | <!-- boundary:installed-modules --> | <!-- wagoe:installed-modules --> |
| Redis pub/sub channel | boundary:realtime:bus | wagoe:realtime:bus |
| Logger name (logback) | boundary | wagoe |
| Repositories | thijs-creemers/boundary{,-examples} | wagoebv/wagoe{,-examples} |
| Sites | boundary-app.org, get.boundary-app.org | wagoe.org, get.wagoe.org |
docs.boundary-app.org is retired; the documentation is folded into
wagoe.org/docs. The GitHub repository transfers preserve history
and leave redirects in place, so existing clones keep working until you update
the remote.
Not renamed, on purpose. "Boundary" is also an architecture term in this
codebase, and those uses are unchanged: the FC/IS boundary rules (ADR-021),
the persistence / HTTP / API / DB boundary, the boundary-check step,
boundary conditions and boundary testing, and "System Boundary" in the PRD.
A bb check:no-boundary gate guards the renamed token families and treats the
prose word as report-only for exactly this reason.
Framework Quality (Phase 0–2, 2026-07) security hardening:
boundary-user: JWT algorithm pinned and startup fails fast on a weak secret — a JWT_SECRET of ≥32 chars is now required, with a separate CSRF secret (BOU-163, #253).boundary-user: MFA TOTP secrets are encrypted at rest and backup codes are hashed (BOU-162, #252).boundary-user: IDOR closed on the user API routes (/users, /users/:id) — a caller can no longer read or modify another user by id (BOU-190, #280).boundary-user: sessions are rotated on password and role change, defeating fixation and stale-privilege reuse (BOU-191, #281).boundary-user: user-management web routes are mounted behind authz guards and the web user-detail page is admin-only (BOU-197, #286, #287).boundary-platform: 5xx responses no longer leak exception internals, and the admin error flash no longer echoes raw exception messages (BOU-161, BOU-182, #250, #260).boundary-platform: hardened security response headers and session-cookie attributes; brute-force lockout and session-fixation coverage; a dedicated authz negative-path suite (RBAC / IDOR / cross-tenant) (BOU-168, #272, #274, #275).boundary-jobs: reliable Redis dequeue — jobs are no longer lost when a worker crashes mid-dequeue (BOU-160, #248).boundary-cli: boundary agents update (+ generated bb agents:update task) refreshes the framework-owned sections of a project's AGENTS.md after a Boundary upgrade. The marker-delimited blocks (gen:fc-is, gen:naming, gen:pitfalls, boundary:available-modules) are re-rendered from the installed CLI's template and spliced in place; everything outside the markers — team notes, custom sections — is left untouched, the installed-modules block is treated as project state, and installed modules stay removed from the refreshed available table (mirroring boundary add). --check exits 1 when the file is stale, for CI. Idempotent (#236).clojure -M:bench hotpaths, dev/boundary/bench/hotpaths.clj) measuring current-vs-proposed implementations for each performance-assessment finding: raw vs compiled Malli validation, reflective vs protocol logger dispatch, DB result-set case-conversion passes, and i18n marker resolution. Notable negative result recorded: memoizing case-conversion keys is slower than plain str/replace — the DB-layer fix targets pass elimination, not caching (#232).boundary-cli: The generated AGENTS.md template now opens the "Adding new functionality" workflow with Step -1 — check existing Boundary modules FIRST: run boundary list modules and prefer boundary add <module> + its ports before writing custom code. Coding agents working in bootstrapped projects were reimplementing functionality that existing modules (auth, storage, jobs, email, cache, search, payments, …) already provide (#232).Framework Quality (Phase 0–2, 2026-07):
boundary-observability: a Prometheus metrics adapter with a GET /metrics scrape endpoint; a backend-agnostic tracing port (ITracer + the with-span macro) with no-op and logging adapters; and an OpenTelemetry OTLP exporter for both traces and metrics, plus automatic per-request HTTP spans and per-job worker spans. One vendor-neutral OTLP/HTTP exporter feeds any OTel backend (SigNoz, Grafana Tempo, Jaeger, Datadog-via-OTel) (BOU-174, #304, #305, #306).boundary-storage: a Google Cloud Storage adapter (V4 signed URLs); a :boundary/storage Integrant key dispatching :local / :s3 / :gcs; and real HMAC-signed, expiring URLs for the local adapter (BOU-206, #308).boundary-email: an in-memory EmailQueueProtocol implementation (bounded retry); :boundary/email + :boundary/email-queue Integrant keys; and user welcome mail routed through the email lib (BOU-206, #309).boundary-jobs: a DB-backed job queue adapter (durable background jobs without Redis) with transactional outbox enqueue (BOU-181, #299, #300).worker run mode (no HTTP listener), and a production configuration guard (BOU-173, #298).boundary-scaffolder: module namespace + path parameterization via --base-ns, and an app-first bb scaffold integrate flow (BOU-205, #302, #303).check:poms (published-POM boundary-dep completeness) with a publishable boundary-shared-ui (BOU-202, #289); a check:test-tags gate with test-pyramid tag backfill across all libs (BOU-166, #262–#266); a check:test-meta gate (BOU-184, #255); and a full-system boot test against embedded PostgreSQL (BOU-183, #261).examples/todo) with CI smoke, 7 missing library READMEs, expanded module AGENTS.md guides, and multi-tenancy usage docs relocated to the tenant README (BOU-175, BOU-201, #295, #296, #297, #292).m/validate/m/explain with a raw schema re-parse the schema and rebuild the predicate on every call — measured 8.6–9.9× overhead. All 121 static-schema call sites across 43 files (login, per-request handlers, per-WebSocket-message :pre checks) now use m/validator/m/explainer defs compiled at namespace load. boundary.core.validation memoizes validator/explainer/decoder compilation for schema-as-argument callers, and the scaffolder template emits compiled validators in generated modules.boundary-platform: reflective (.info logger …) interop in the HTTP/service interceptors (fired on enter+leave of every request) replaced with ILogger protocol calls — measured ~90× per call; *warn-on-reflection* enabled in both namespaces.boundary-platform: DB result keys are converted snake→kebab in the next.jdbc builder-fn (as-unqualified-kebab-maps, column names converted once per result set) instead of a second full per-row map rebuild — ~1.7× on 100-row results; redundant third conversions removed from user db->user-entity and six admin service sites.boundary-admin: entity config and table metadata cached in the long-lived SchemaRepository component — previously every admin page issued 2×N information_schema queries (N = registered entities). reset-cache! provided for post-migration invalidation.boundary-platform: static-resource middleware no longer does a classloader lookup on every request — gated to GET/HEAD URIs with a file extension; duplicate query/form param parsing removed (global wrap-params dropped in favour of reitit's parameters-middleware).boundary-i18n: resolve-markers postwalk replaced with a structural-sharing transform that returns original nodes when no descendant changed — 82.4µs → 25.0µs (3.3×) on a 50-row table page, full render ~1.9×; translate/t no longer re-runs satisfies? per locale per key.boundary-audience / boundary-user: N+1 write patterns batched. save-memberships!: exists-SELECT + single-row INSERT per user (50k-user audience = 100k statements) → one SELECT + in-memory diff + chunked 500-row multi-row INSERTs (~102 statements, portable H2/PG). update-users-batch: per-user UPDATEs → next.jdbc/execute-batch! grouped by column shape. Signatures, return values and transaction semantics unchanged.boundary-platform: CSRF fast paths — http-csrf-protection short-circuits before any binding/cookie work when disabled (the default), and wrap-csrf decides at wrap time, returning the raw handler unwrapped. When enabled, a request already carrying a valid token for the current binding gets it re-exposed instead of a fresh CSPRNG draw + HMAC sign per request (tokens have no expiry/rotation requirement; binding model, cookie attributes, constant-time compare and 403 semantics unchanged — security suite green).boundary-platform: correlation ids generated from ThreadLocalRandom instead of UUID/randomUUID's shared, contended SecureRandom (internal trace ids, not security tokens); http-request-metrics no longer computes a per-request duration it then discards; interceptor leave/error phases use rseq instead of reverse; version headers built once at wrap time instead of per response.boundary-user: JWT signing secret resolved from the environment once per process instead of a System/getenv call on every token sign/verify.boundary-jobs: worker Redis heartbeat throttled to :heartbeat-interval-ms (default 5000 ms, key TTL 60 s) instead of one round-trip per loop iteration — under load the loop spins once per job.Framework Quality (Phase 0–2, 2026-07) — architecture & FC/IS:
core/ into the shell; core/ may no longer throw or hold mutable state, enforced by check:fcis. Follow-ups: audience filter/composition validation moved to the shell (BOU-185, #256), the validation rule registry split into a pure core + stateful shell (BOU-188, #258), and legitimate core exemptions reclassified inline (BOU-186, #257).boundary-shared-ui, dissolving admin↔user (BOU-193/194, #283); tenant HTTP middleware relocated out of platform and its wiring moved to the app layer (BOU-198/200, #284, #291); parallel search stacks merged into libs/search (BOU-169, #276).user/ports (BOU-170, #277); dead admin http/support helpers removed (#293); admin core/ui and shell/http split into focused namespaces behind a facade (BOU-195, #288).boundary-observability: HTTP request metrics now emit through the real IMetricsEmitter (request count, error count, latency histogram) instead of a no-op stub, so they reach any active provider (BOU-208, #307).boundary-platform: PostgreSQL session settings (statement_timeout, TimeZone=UTC, ApplicationName) now reach every pooled connection via JDBC URL properties — the previous SET statements only affected the single pooled connection that happened to execute them, leaving the statement-timeout guard effectively unset on the rest of the pool. reWriteBatchedInserts=true enabled while at it (#232).build.clj hardcoded 9 source dirs; the list now derives from deps.edn :paths, so new libs are packaged automatically. Also enables -Dclojure.compiler.direct-linking=true and resolves a LICENSE file-vs-directory merge conflict between dependency jars (#232).boundary-email / boundary-external: Attachment schemas used :bytes, which is not a schema in Malli's default registry — valid-email?, valid-email-input? and explain-email-errors threw :malli.core/invalid-schema on every call (latent: zero callers; surfaced by compiling validators at namespace load). Fixed to bytes? with regression tests (#232).boundary-user: find-active-users-by-role, find-users-created-since and find-users-by-email-domain ran SELECT … ORDER BY created_at DESC with no LIMIT — unbounded memory/latency as data grows. All three now default to the platform's max-pagination-limit (1000) via build-pagination; new {:limit :offset} options arities added to IUserRepository (base arities delegate), nil limit/offset guarded at the SQL assembly point.Framework Quality (Phase 0–2, 2026-07):
boundary-observability: Prometheus metric/label names that collide after sanitization (e.g. :http.requests and :http-requests both → http_requests) are handled deterministically — a later colliding metric registration is logged and ignored (first wins), and colliding label keys within a series are de-duplicated — instead of rendering invalid exposition (BOU-207, #310).boundary-audience: the account-tenure :neq filter now maps to SQL <> (BOU-189, #259).boundary-platform: enum CHECK constraints are now idempotent across H2 reconnects (#273).boundary-payments: Stripe checkout 400 on an invalid success_url (BOU-148, BOU-149). create-checkout-session POSTed whatever stripe-checkout-params produced. A broken upstream return-URL config (e.g. an unset PUBLIC_BASE_URL) reached Stripe two ways: an empty string when the :redirect-url fallback was also blank (400 parameter_invalid_empty, BOU-148), or a scheme-less relative path like /web/license/payment/return?… when the configured URL was left relative (400 url_invalid, BOU-149). The adapter now validates the resolved success_url/cancel_url before the Stripe call: anything that is not an absolute http(s) URL throws a :config-error ex-info naming the offending param, its value, and the fix (provide an absolute return URL / set PUBLIC_BASE_URL in acc/prod), so the misconfiguration is actionable instead of surfacing as an opaque provider error.boundary-payments: Stripe webhook 500 on unmapped event types (BOU-147). process-webhook threw an :internal-error ex-info for any Stripe event whose type is not one of the four generic payment_intent.* mappings — including checkout.session.completed (the primary paid-flow event), checkout.session.expired, and charge.dispute.created. In the billing webhook handler that throw is uncaught and returns HTTP 500, so a freshly-connected Stripe endpoint 500s on every delivery Stripe fires by default and Stripe retries for days. The consumer's event-action was already designed to route these by payload type (or ignore them), but the throw fired first. process-webhook now returns a result with :event-type nil and the full payload for any unmapped-but-parseable event instead of throwing, so the handler acknowledges with 200 and the billing layer routes/ignores by payload type. Only the four payment_intent.* types still resolve to a framework event-type.boundary-platform: Rate limiting wired into the default route pipeline (BOU-87). New config-driven http-rate-limit-protection interceptor runs in the default HTTP stack and reads its policy from :boundary/http :rate-limit {:enabled? :limit :window-ms} (env HTTP_RATE_LIMIT, HTTP_RATE_LIMIT_WINDOW_MS); the wiring injects the :boundary/cache so an active Redis cache yields a fixed-window limit shared across replicas. Enforcement is opt-in (default off) so an upgrade cannot start 429-ing existing consumers — enabled only in the bundled dev config (single-node, 1000/min); the prod/acc configs ship it disabled (enable together with an active Redis cache), and test leaves it off. Caveat: with no active cache the limiter falls back to a per-process counter — correct on a single node only; across N replicas the effective global limit is limit × N, and the wiring logs a warning at startup. The existing fixed-arg http-rate-limit form remains for explicit per-route use.
boundary-jobs: Multi-instance hardening (BOU-88). (1) A dequeued job whose type has no handler on the local worker is now re-enqueued for another instance instead of being silently dead-lettered; it is failed terminally with a NoHandlerError only after it has gone unhandled for :max-requeue-age-ms (default 300000 / 5 min). The re-enqueue is delayed (parked in the scheduled set :requeue-delay-ms ahead, default 1000) so a handlerless worker cannot reacquire the job on its next immediate poll and spin. Give-up is age-based, not attempt-based (tracked via [:metadata :first-missing-at]): a wall-clock window is independent of fleet size and load, so wrong-worker misses can't drop a job a slow handler-owning worker hadn't polled yet (:max-requeues, default 10000, remains only as a runaway backstop). A worker created with an empty handler registry logs a loud warning at startup. (2) Scheduled-job promotion is now an atomic claim so concurrent workers can't both move (and run) the same due job: the Redis adapter uses ZREM as the claim (only the worker whose ZREM returns 1 enqueues) and the in-memory adapter uses swap-vals!. Both process-scheduled-jobs! implementations now return the count actually promoted.
boundary-tenant: Per-tenant provisioning of the compliance/vbar/import entity tables plus a background-job schema-iteration helper (ZZP-86). Four new tables (compliance_snapshots, compliance_changes, vbar_assessments, import_batches) join tenant-scoped-tables, so provision-tenant! now copies them into each new tenant_<slug> schema. New sync-tenant-schemas! is the idempotent upgrade/sync path: because provision-tenant! only copies tenant-scoped tables when creating a schema and returns early for existing ones, previously-provisioned tenants would otherwise miss tables added in a later release. sync-tenant-schemas! re-copies every tenant-scoped table into every existing tenant schema via CREATE TABLE IF NOT EXISTS (existing tables untouched, only missing ones created — safe to re-run); no-op on non-PostgreSQL. Run it on deploy after extending tenant-scoped-tables. New boundary.tenant.shell.tenant-iteration/for-each-tenant-schema is the background-job analogue of the HTTP wrap-tenant-schema middleware: it runs a 1-arg fn once per provisioned tenant schema with that tenant's search_path pinned via with-tenant-schema, so jobs touching tenant-scoped tables get the same connection-pinned isolation as the request path (without it a job runs under search_path = public and sees only the empty public tables). Per-tenant failures are isolated and counted — one tenant's error never aborts the others — returning {:processed n :failed n :results [...]}.
boundary-platform: Graceful connection draining on shutdown (BOU-86). The :boundary/http-server component configures Jetty's GracefulHandler and setStopTimeout so that on stop the server stops accepting new connections, rejects new requests with 503, and lets in-flight requests finish before halting — eliminating cut requests during rolling restarts. New config knob :boundary/http :drain-timeout-ms (env HTTP_DRAIN_TIMEOUT_MS): default 30000 ms in prod/acc, 5000 in dev, 1000 in test; 0 or nil disables draining. Set the window above the load balancer's deregistration delay for zero-downtime rollouts.
boundary-platform: The in-memory rate-limit fallback is now heap-bounded and the bundled prod/acc configs ship rate limiting disabled (BOU-87 follow-up). Previously check-rate-limit-memory only pruned a client's timestamp vector when that same client returned and never evicted old client keys, so high-cardinality client ids (rotating API keys, many remote addresses) could grow the process-global state map without limit on a long-running node — and prod/acc enabled rate limiting by default while :boundary/cache was inactive, silently selecting that fallback in production. The fallback is now bounded by a hard cap (max-tracked-clients = 10000): before recording a new client at the cap it sweeps clients with no in-window requests and, if the map is still full (every client in-window), evicts the least-recently-active client — so even a sustained stream of fresh client ids can't grow it past the cap. The read-modify-write is atomic inside one swap!. prod/acc default to :enabled? false and wire HTTP_RATE_LIMIT_ENABLED (via aero #boolean) so operators can enable it with an env var once an active Redis cache is in place, rather than editing EDN.
boundary-platform: The http-rate-limit interceptor now actually blocks over-limit requests (BOU-87). Its rejection set :response but not :halt?, so run-pipeline continued to the downstream ring-handler, which overwrote the 429 with the handler's 200 — the limit was counted but never enforced. The rejection now sets :halt? true (matching http-csrf-protection), short-circuiting the pipeline.
boundary-payments: Stripe Checkout Session creation is now diagnosable and blank-safe (BOU-127, #216). create-checkout-session logs the Stripe error reason (type/code/param/message) on any non-2xx response instead of only status=%d id=%s, so a 400 is no longer opaque. stripe-checkout-params builds success_url/cancel_url blank-safely — an empty/whitespace override (e.g. an unset PUBLIC_BASE_URL upstream) no longer wins over redirect-url via (or "" redirect-url) and produces an empty success_url that Stripe rejects with a 400.
boundary-push: Comprehensive developer documentation for the push notification library (BOU-44). AGENTS.md now covers all five protocols (IPushService, IFCMProvider, IAPNsProvider, IDeviceTokenStore, IPushAnalyticsStore), Integrant wiring keys, HTTP routes, job handler arg shapes, HMAC callback flow, DB table overview, and REPL smoke checks. README.md corrects the Integrant configuration (actual ig/init-key dispatch keys), fixes API function names (register-device! / unregister-device! / get-user-devices), and adds the missing push_analytics_events migration to the DDL reference.
boundary-devtools: Unified error-code catalogue — bb guide error BND-201 and bb guide error BND-601 now return title, cause, and fix instead of "Unknown error code" (BOU-49). error_catalog.edn (libs/devtools/resources/) is the single source of truth consumed by both the JVM runtime (boundary.devtools.error-codes, moved out of core/ so I/O is permitted) and the Babashka CLI (bb guide). BND-0xx tooling codes are retired in favour of the BND-1xx..7xx range scheme: BND-1xx configuration, BND-2xx validation, BND-3xx persistence, BND-4xx auth, BND-5xx interceptor, BND-6xx FC/IS violations, BND-7xx tooling/build (new — circular deps BND-701, admin entity config BND-702, module not wired BND-703, migration version conflict BND-305). bb guide error (no code) lists all codes grouped by category in numerical range order.
boundary-platform: discover-migration-dirs now scans each resolved migration directory after discovery and emits a WARN log for any subdirectory that contains .sql files (e.g. migrations/tenant/). Catches the class of misconfiguration where tenant-scoped migrations are placed inside the public migration root and silently applied to the wrong schema; the warning fires on the first clojure -M:migrate up run rather than producing a hard-to-diagnose data error later.
boundary-admin: Proportional list-view column widths derived from field :type plus a field-name heuristic, replacing the previous even distribution where a boolean column got the same width as a name or description column (BOU-46). Weights: boolean=1, enum/numeric/uuid/json=2, date/instant=3, text=6; string columns default to 3, with name-like fields (name, title, email, …) widened to 4 and long-form fields (description, address, comment, …) to 6. An optional :width key on a field config (a positive integer weight) overrides the computed default. Widths are emitted as proportional width:N% on the table <colgroup> and resolved deterministically at render time — no runtime AI.
boundary-ai: bb ai admin-entity now suggests :width values in generated admin entity EDN configs (BOU-48, layer 3 of the column-width system). The AI is taught the same type/name weight table used by the runtime heuristic and only emits :width for fields whose semantics differ from the default — e.g. :sku, :code, :ref, :barcode get {:width 1} (narrow identifiers) while :description and :name are left without :width because the heuristic already assigns them correct weights. The result is a static :width in the generated EDN with no AI involvement on the hot render path.
boundary-platform: The default HTTP interceptor stack is no longer skipped for :no-doc routes. Interceptor application is now controlled by an explicit per-route :skip-interceptors? flag (set only on genuinely-internal endpoints such as health checks); :no-doc once again means only "exclude from the Swagger spec". As a result every /web route now runs the full stack — request logging, metrics, error reporting, correlation header, CSRF, and security headers — where previously it ran none. The most visible effect is that HTML pages now carry the security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, …) they were silently missing. The shipped CSP allows 'unsafe-inline'/'unsafe-eval' for HTMX/Alpine and all UI assets are self-hosted, so rendering is unaffected (BOU-43).boundary-platform: Replaced the ring/ring-anti-forgery dependency with buddy/buddy-core. CSRF tokens are generated and verified directly (HMAC-SHA256, constant-time) rather than via Ring's session-backed anti-forgery middleware, which did not fit the framework's cookie/header session model (BOU-43).boundary-cache: The Redis adapter now serializes values with Nippy instead of JSON, fixing class java.lang.String cannot be cast to class java.time.temporal.Temporal for cached java.time values (BOU-47). JSON is lossy — Temporal values became ISO-8601 strings, keywords became strings, and sets became vectors — and the loss surfaced only against Redis since the in-memory adapter stores values by reference. Nippy round-trips keywords, sets, ratios and java.time/Temporal values intact, matching the in-memory adapter.boundary-cache: The Redis adapter treats unreadable entries (values written by the previous JSON format, or otherwise non-Nippy bytes) as a cache miss instead of throwing, so the cache self-heals on rollout. Note: the on-the-wire format changes from JSON to Nippy; flushing the cache namespace on deploy is still recommended to avoid log noise from stale reads (BOU-47).boundary-platform: Real CSRF protection for session-authenticated, state-changing requests (POST/PUT/DELETE/PATCH), replacing a stub that always passed (BOU-43). Enforcement is opt-in — the library default is :enabled? false, so upgrading the framework cannot start rejecting requests from consumers that do not yet emit tokens; each app enables it explicitly after emitting tokens (BOU-56). When enabled, a request is validated — 403 on a missing or invalid token — when the path is not exempt and the request is either session-authenticated (session-token cookie / X-Session-Token header) or a /web route. This protects /web, /web/admin, and any session-authenticated /api route; token-auth API clients that send no session cookie are not CSRF-vulnerable and are not checked. Details:
base64url(nonce).base64url(HMAC-SHA256(secret, nonce ‖ binding))); authenticated requests bind to the session, unauthenticated /web flows (login, register, MFA) bind to a SameSite=Strict csrf-session cookie minted on the page GET.(boundary.platform.core.csrf/hx-headers) merged onto an element (e.g. <body>, inherited by all hx-* requests) or from the shared page layout's <meta name="csrf-token"> tag plus a global htmx:configRequest listener that attaches X-CSRF-Token to every HTMX request. Plain <form method=post> forms include a hidden field via (boundary.platform.core.csrf/hidden-field).:boundary/http :security :csrf {:enabled? :secret :exempt-paths}; the library default is opt-in (:enabled? false). The bundled app enables it explicitly in dev/prod/acc (prod/acc require JWT_SECRET from the environment); the secret otherwise falls back to JWT_SECRET. List webhooks/callbacks (which cannot carry a token) under :exempt-paths (a trailing /* matches by path-segment prefix). Startup fails loud: if CSRF is enabled with a blank secret the system wiring throws and the app refuses to boot, rather than starting with the interceptor failing open (running unvalidated). BREAKING (BOU-56): this replaces the previous warn-and-continue behavior — an app that set :enabled? true but left JWT_SECRET/:secret unset used to boot (CSRF silently disabled) and will now fail to start. Set the secret in the environment before upgrading.boundary-audience: New audience segmentation library (libs/audience/) with declarative, rule-based segment definitions. Features include:
defaudience macro for code-defined segments with seven built-in filter types (demographics, location, role, account-tenure, last-active, behavior, feature-usage)audience_memberships table with configurable per-segment TTLfilter->sql and filter->predicate multimethodsIAudienceResolver, IAudienceRepository, and IAudienceCache componentsboundary-realtime: Optional :on-open callback for websocket-handler — (fn [connection-id]) invoked after a successful connect, for subscribing connections to topics based on the authenticated user's roles. Exceptions thrown by the callback are logged and swallowed, so they do not abort the connection.boundary-push: New push notification library (libs/push/) with multi-platform delivery via FCM (Firebase Cloud Messaging) and APNs (Apple Push Notification service). Features include:
defpush macro for declarative notification definitions with i18n locale maps, deep links, priority, TTL, collapse keys, and retry configurationIFCMProvider, IAPNsProvider) behind unified IPushService orchestratorsendAsync + CompletableFuture for both FCM and APNsboundary-jobs/api/push/devices), callback (/api/push/callback), stats (/api/push/stats/:id)push_device_tokens, push_send_log, push_analytics_events with multi-tenant supportboundary-user: Welcome email on admin user creation — optional send-welcome checkbox triggers email via ISmtpProvider with graceful failure handling.boundary-user: Dashboard extensibility via :dashboard-extra-cards config for injecting custom Hiccup cards into the user dashboard.boundary-ui-style: Cross-page toast notification system via X-Toast response header + sessionStorage, works across all page layouts (base, pilot, admin-pilot).boundary-cache: Deterministic LRU eviction — replaced timestamp-based ordering with monotonic access counter. Fixes non-deterministic eviction when entries are created within the same millisecond.boundary-user: XSS in create-user-htmx-handler inline <script> — added escape-js-string to sanitize return-to URL, toast JSON, and user name before interpolation. Prevents quote-breaking and </script> tag injection.boundary-admin: Toast JSON injection via entity labels in delete/bulk-delete handlers — added escape-json-string to sanitize label values in X-Toast and HX-Trigger headers.boundary-admin: Split-table soft-delete now correctly writes deleted_at to both primary and secondary tables in a transaction, fixing column "deleted_at" does not exist errors.boundary-admin: Added config validation for split-table entities missing :create-redirect-url, failing early with a clear error instead of a StreamableResponseBody crash.boundary-admin: Added log/error to create-entity exception handler (previously swallowed silently).boundary-admin: Added missing deleted_at column to users test DDL for embedded PostgreSQL integration tests, fixing 12 pre-existing test errors.boundary-user: Restored 500 status code for server errors in create-user-htmx-handler (was incorrectly returning 200).boundary-user: Fixed arity mismatch in create-user-htmx-handler test calls — handler signature changed to [user-service email-sender config] but tests were not updated.boundary-ui-style: Removed duplicate XHR monkey-patch from admin-ux.js — components.js already handles X-Toast capture for all bundles.boundary-ui-style: Increased horizontal padding on table pagination for better alignment.ci: Replaced :local/root dep in bb.edn with direct :paths entry for libs/tools/src, preventing deps.clj from triggering a Clojure tools download that times out on CI runners.boundary-admin: Auto-introspect secondary table fields when :split-table-update is configured, so split-table entities no longer require manual field definitions (#158).boundary-admin: Auto-expand SELECT columns for join queries in split-table setups, ensuring all fields from both tables are fetched (#158).boundary-admin: Auto-hide tsvector generated columns from entity forms and list views (#158).boundary-admin: Skip required validation for boolean fields, which default to false rather than NULL (#158).boundary-admin: Fixed swapped primary/secondary table alias mapping in resolve-query-config, which caused wrong SQL column qualifiers for split-table entities (#158).boundary-admin: Fixed snake_case→kebab-case mismatch in SELECT deduplication that caused duplicate columns in split-table join queries (#158).boundary-admin: Fixed split-table SELECT auto-expansion assigning columns to wrong table alias when :secondary-table maps to the :from table in query-overrides (e.g., a.tenant_id instead of u.tenant_id). Alias is now resolved by matching :secondary-table against :from/:join table names (#158).boundary-admin: Embedded PostgreSQL test infrastructure (io.zonky.test/embedded-postgres) for admin-user-operations-test. Split-table tests with tenant_id and other PG-specific columns now run against a real PostgreSQL instance instead of H2, fixing 8 pre-existing test errors.boundary-admin: New test helper namespace boundary.admin.test.embedded-pg with start!/stop!/db-context/with-embedded-pg for reusable embedded PG lifecycle in tests.ci: Removed non-existent :db/h2 alias from all CI test commands. H2 and embedded PostgreSQL deps are already in the :test alias; the phantom alias was silently ignored but produced warnings.cheshire version in boundary-cli from 5.12.0 to 6.2.0 (matches rest of monorepo).org.clojure/clojure in :build alias from 1.12.3 to 1.12.4.boundary-admin: schema_repository/get-entity-config now uses :table-name from manual entity config when fetching table metadata, so entities whose key differs from their table name (e.g. :users → auth_users) resolve correctly (BOU-28).boundary-admin: bulk-delete-entities now targets :soft-delete-table instead of :table-name when soft-deleting, fixing bulk deletes in split-table setups (BOU-28).boundary-admin: update-entity and update-entity-field now use execute-update! for DML statements instead of execute-one!, fixing UPDATE execution in both split-table and single-table paths (BOU-28).boundary-admin: Added :soft-delete true to the default users admin entity config so soft-delete is enabled out of the box (BOU-28).boundary-tools: bb create-admin now works in freshly generated projects (BOU-27). The command previously shelled out to clojure -M:cli:db which requires boundary.cli — a monorepo-only namespace never included in published libraries. Replaced with clojure -M:user-cli which calls boundary.user.shell.cli-entry/run-cli! directly via -e eval, requiring no unpublished code.boundary-cli: Generated deps.edn now includes a :user-cli alias with all four JDBC drivers (SQLite, PostgreSQL, H2, MySQL) so bb create-admin works regardless of which database adapter the project is configured to use.boundary-cli: Generated config.clj now defines user-validation-config, which boundary.user.shell.cli-entry resolves at runtime via requiring-resolve.boundary-tools: bb create-admin passes the target environment via BND_ENV environment variable instead of -J-Denv=, matching how boundary.config/load-config actually reads the active profile.boundary-user: MFA QR code is now generated locally using the ZXing library (com.google.zxing/core and com.google.zxing/javase 3.5.3) instead of calling the external api.qrserver.com service. generate-qr-code-data-url returns a data:image/png;base64,… URL that works in <img src> without any network dependency (#148).build (all 25 libraries): Each library JAR now embeds a cljdoc.edn file containing {:cljdoc/root "libs/<name>"}. Without this hint cljdoc defaulted to the repo root and could not find source files located under libs/{name}/src, breaking all Clojars cljdoc links (BOU-26, #149).1.0.1-alpha-21 to re-align lockstep versioning.boundary-cli: boundary new now generates a full boundary-tools task suite in bb.edn instead of a minimal 3-task config. The old template used broken (clojure ["-M:repl-clj"]) syntax that caused FileNotFoundException: [-M:repl-clj] on bb repl.boundary-cli: Generated deps.edn now uses :repl alias (consistent with generated-project convention; monorepo uses :repl-clj).1.0.1-alpha-20 to re-align lockstep versioning.boundary-tools: bb scaffold generate now works in projects created from boundary-starter — scaffolder is injected via -Sdeps instead of requiring it on the classpath.boundary-tools: bb smoke-check no longer fails in generated projects — removed monorepo-only :docs-lint alias from required checks.boundary-tools: bb check linting no longer includes libs/*/src libs/*/test paths when not in the monorepo.boundary-tools: bb install-hooks gives a friendly message instead of a Java exception when run outside a git repository.boundary-tools: AI CLI (bb ai) falls back to environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, OLLAMA_URL) when config has :provider :no-op or no AI config is present.boundary-ai: OpenAI-compatible base URLs with a trailing /v1 suffix no longer produce double /v1/v1/chat/completions paths.boundary-scaffolder: Generated deps.edn now includes :clj-kondo and :migrate aliases with all four database drivers (SQLite, PostgreSQL, H2, MySQL).boundary-devtools — DX Vision: 6-phase developer experience overhaulBND-*) with structured messages, ADRs for devtools guidance engine, REPL command center, dev dashboard, error experience, and progressive learning (ADR-024 through ADR-029).boundary.devtools.core.introspection), schema exploration (schema-tools), documentation lookup (documentation), and guidance engine (guidance). REPL namespace (boundary.devtools.shell.repl) with unified API.boundary.devtools.core.auto_fix), stacktrace parser, FC/IS checker, HTTP error middleware, and REPL error handler.localhost:9090) with pages for system overview, routes, schemas, database, errors, requests, and docs. Hiccup-rendered with custom CSS, served via Ring.boundary.devtools.core.recording), route testing (router), and rapid prototyping (prototype). Shell adapters for file-based recording persistence and route simulation.boundary-ai — REPL AI integration (Phase 6)explain-code, suggest-refactor, generate-docs in boundary.ai.shell.repl.boundary.ai.core.prompts.boundary-cache — LRU eviction bug (#137)boundary.cache.shell.adapters.in_memory to correctly identify the least-recently-used entry when multiple entries share the same timestamp.boundary-tools — BOU-15 deprecated wrapper usage scanner (BOU-15):refer'd deprecated symbols: normalize-require-spec now extracts :refer [sym ...] vectors alongside :as aliases. A new extract-referred-symbols function maps directly referred symbol names to their source namespace. find-qualified-call-sites runs a second regex pass for bare (symbol ...) call sites, so usage like (:require [boundary.search.core.index :refer [build-document]]) is no longer silently missed.find-qualified-call-sites now unconditionally searches for (namespace/symbol ...) patterns regardless of whether the file has an alias or :refer entry. Calls like (boundary.search.core.index/build-document ...) are now correctly reported.ci — E2E job disablede2e CI job with if: false to reduce pipeline run time. Tests can be run manually when needed.boundary-tools into libs/tools/ to follow monorepo convention. Removed redundant top-level boundary-tools/ directory.boundary-e2e — Admin UI end-to-end test suite (BOU-10)boundary.e2e.helpers.admin) with login-as-admin!, login-as-user!, two-phase HTMX settle waiting (install-htmx-settle-listener! / await-htmx-settle!), and table/form query utilities.with-fresh-seed fixture for isolated H2 state per test.platform — Compile-time PostgreSQL class references(:import [org.postgresql.util PGobject]) and (instance? org.postgresql.util.PSQLException ...) from boundary.user.shell.service, boundary.tenant.shell.persistence, and boundary.tenant.shell.invite-persistence. Replaced with runtime class name checks so the REPL starts without the :db alias on the classpath.admin — Table view UX improvementsadmin — Compact table view layoutadmin — Collapsible sidebaradmin-ux.js to comply with Content Security Policy.admin-ux.js now loads before alpine.min.js so the sidebar store is registered before Alpine initializes.boundary-e2e — end-to-end test suite for login sequence/web/login, /web/register, and /api/v1/auth/* — browser automation + API testing via spel (Playwright Java wrapper). No Node.js/npm/TypeScript introduced.libs/e2e/ with com.blockether/spel dependency, isolated behind opt-in :e2e alias — normal clojure -M:test runs are unaffected.bb e2e: orchestrator task that starts the app in :test profile on port 3100, runs the kaocha :e2e suite, and tears down the server.bb run-e2e-server: standalone task for manual debugging against the test-profile server.POST /test/reset endpoint (behind :test/reset-endpoint-enabled? config flag) that truncates H2 and re-seeds baseline tenant/users via production services. Guarded by startup assertion (throws in prod/acc) and bb doctor check.:e2e suite in tests.e2e.edn with ^:e2e metadata filtering.e2e job in .github/workflows/ci.yml with Playwright browser cache.boundary-user — 5 auth/session bugs discovered by e2e tests[:session :user :id] instead of [:user :id] from the request — all 4 MFA endpoints (setup, enable, disable, status) always returned 500. Fixed by reading (:user request) directly.login-submit-handler checked for "on" but the ui/checkbox component submitted "true" — remember-me never activated. Fixed by accepting any truthy form value.string->instant in boundary.core.utils.type-conversion did not handle java.time.OffsetDateTime (returned by H2 for TIMESTAMP WITH TIME ZONE columns) — caused NPE in is-session-valid?, bouncing users back to login after successful authentication. Fixed by adding OffsetDateTime handling.should-allow-login-attempt? and calculate-failed-login-consequences existed in the core layer but were never called from the service layer. Fixed by adding a service-level lockout gate that checks the threshold before delegating to authenticate-user. Only true lockout (:retry-after present) short-circuits — deactivated/deleted accounts fall through to the normal auth flow to preserve their own error semantics.+, /, = characters that caused Jetty 400 errors on GET/DELETE /api/v1/sessions/:token. Fixed by switching generate-session-token to URL-safe base64 (Base64/getUrlEncoder without padding). Breaking change: existing sessions with old-format tokens will fail validation — users must re-login after deploy.boundary-tools — 4 new developer helper toolsbb doctor — Config Doctor (rule-based)config.edn and project files — no AI required.env-refs (error): detects #env VAR references in the :active section without #or fallback that are not set in the environment.providers (error): validates :provider values against known sets (logging, metrics, error-reporting, payments, AI, cache).jwt-secret (error): verifies JWT_SECRET is set when the user module is active.admin-parity (warn): checks that admin entity EDN files exist in both dev/admin/ and test/admin/.prod-placeholders (error): flags placeholder values (company.com, example.com, TODO, CHANGEME) in prod/acc configs.wiring-requires (warn): verifies active Integrant modules have their module-wiring require in wiring.clj.bb doctor [--env dev|prod|acc|all] [--ci]. --ci exits non-zero on any error for CI pipelines.boundary.tools.doctor.bb setup — Config Setup Wizard (templates + optional AI)--database postgresql --payment stripe), or AI-powered natural language (bb setup ai "PostgreSQL with Stripe").resources/conf/dev/config.edn, resources/conf/test/config.edn, and .env.example from template fragments.boundary.ai.shell.cli-entry setup-parse; falls back to interactive if no AI provider is available.boundary.tools.setup.bb scaffold integrate — Module Integration (rule-based)bb scaffold generate: patches deps.edn (source/test paths), tests.edn (per-library suite), and wiring.clj (module-wiring require).--dry-run mode previews all changes without writing files.bb scaffold integrate <module> and bb scaffold:integrate <module>.boundary.tools.integrate.bb ai admin-entity — Admin Entity Generator (AI-powered)bb ai admin-entity "products with name, price, status").resources/conf/dev/admin/ and includes them as examples in the prompt for style consistency.--yes flag for non-interactive use.resources/conf/dev/admin/<entity>.edn and resources/conf/test/admin/<entity>.edn.#include directive).boundary.tools.admin_entity. Clojure-side additions:
boundary.ai.core.prompts: admin-entity-messages, setup-parse-messages prompt builders.boundary.ai.shell.service: generate-admin-entity, parse-setup-description orchestration functions.boundary.ai.shell.cli-entry: cmd-admin-entity, cmd-setup-parse subcommands.bb.edn: 3 new tasks registered (doctor, setup, scaffold:integrate) + 3 new requires (boundary.tools.doctor, boundary.tools.setup, boundary.tools.integrate).bb ai help text and dispatch updated with admin-entity and setup-parse subcommands.bb scaffold help text and dispatch updated with integrate subcommand.AGENTS.md (root): new tools added to Quick Reference and namespace table.CLAUDE.md: new commands added to Scripting section.boundary-tools/AGENTS.md: comprehensive documentation for all 4 tools with examples, tables, and workflow guides.libs/ai/AGENTS.md: features list updated to 7, service API examples added, 3 new pitfalls documented (#9–#11).boundary-realtime — Ring WebSocket handlerboundary.realtime.shell.handlers.ring-websocket): bridges Ring's map-based ::ring.websocket/listener response to the existing IRealtimeService connect/disconnect lifecycle. JWT authentication via token query parameter; on-open creates adapter and registers connection, on-close/on-error triggers disconnect cleanup.websocket-handler accepts keyword options: :token-param (default "token") and :on-message for optional client→server bidirectional messaging.ring/ring-core 1.15.3 added to libs/realtime/deps.edn.boundary-payments — new libraryIPaymentProvider protocol in boundary.payments.ports with create-checkout-session, get-payment-status, process-webhook, and verify-webhook-signature methods. Implementations: StripePaymentProvider, MolliePaymentProvider, MockPaymentProvider (development/tests).CheckoutRequest, CheckoutResult, PaymentStatusResult (:pending/:paid/:failed/:cancelled), WebhookResult (:payment.paid/:payment.failed/:payment.cancelled/:payment.authorized).boundary.payments.core.provider): cents->euro, normalize-event-type, mollie-status->event-type, mollie-status->payment-status, stripe-event->event-type.payment_intent_data[metadata][checkout_id] for webhook correlation, HMAC-SHA256 signature verification with constant-time comparison, 300s timestamp tolerance, graceful handling of malformed Stripe-Signature headers.get-payment-status, form-POST webhook processing with payment fetch-back verification.:boundary/payment-provider with :provider (:mock/:mollie/:stripe), :api-key, :webhook-secret, :webhook-base-url.payments-module-config in boundary.config, boundary.payments.shell.module-wiring loaded via platform wiring, boundary/payments dependency added to libs/platform/deps.edn.^:unit + ^:integration).libs/payments/deps.edn: standalone library with clj-http, cheshire, malli, integrant, tools.logging.boundary-ai — new library (Phase 19 of Boundary Roadmap)IAIProvider protocol in boundary.ai.ports with complete, complete-json, and provider-name methods. Implementations: OllamaProvider (offline-first, no API key), AnthropicProvider, OpenAIProvider, NoOpProvider (test stub).:fallback provider in :boundary/ai-service; if the primary fails, the fallback is used transparently.bb scaffold ai "<description>" [--yes]): parses a natural language module description into a validated ModuleGenerationRequest spec and delegates to the existing scaffolder pipeline. Preview + confirm by default; use --yes for non-interactive generation.bb ai explain, (ai/explain *e)): reads a Clojure/Boundary stack trace, extracts referenced source files, and returns a structured root-cause + fix-suggestion using framework-specific system prompts.bb ai gen-tests <file>): reads a source file, detects test type (:unit for core/, :contract for adapters/, :integration otherwise), and generates a complete Kaocha-compatible test namespace.bb ai sql "<description>", (ai/sql "...")): translates a natural language query description into HoneySQL map + explanation + raw SQL preview. Auto-discovers schema context from schema.clj files.bb ai docs --module <path> --type agents|openapi|readme): generates AGENTS.md developer guides, OpenAPI 3.x YAML, or README.md from source files.boundary.ai.shell.repl): (ai/explain *e), (ai/sql "..."), (ai/gen-tests "path/to/file.clj") — bind service once with (ai/set-service! system-service).:boundary/ai-service with :provider, :model, :base-url/:api-key, and optional :fallback sub-config.Message, AIRequest, AIResponse, ProviderConfig, AIConfig.boundary.ai.core.*): prompts.clj (system + user prompt builders for all 5 features), context.clj (module name extraction, stack trace parsing, function signature discovery, schema context), parsing.clj (JSON response parser, module spec → CLI args converter, SQL + test code extractors).^:unit + ^:integration).libs/ai/AGENTS.md: 7-section developer guide covering provider setup, REPL usage, CLI reference, common pitfalls (8 patterns), testing commands.libs/ai/deps.edn: standalone library with clj-http, cheshire, malli, integrant, tools.logging..github/workflows/ci.yml: test-ai job added (needs: lint); libs/ai/src added to the lint step; test-ai wired into test-summary..github/workflows/publish.yml: boundary-ai added to Layer 4 (standalone, no inter-library dependencies); updated release body and step summary.scripts/ai.clj: new Babashka script — bb ai explain, bb ai gen-tests, bb ai sql, bb ai docs.scripts/scaffold.clj: bb scaffold ai "<description>" subcommand added.bb.edn: ai task added.AGENTS.md and CLAUDE.md: ai added to library listing, test command reference, Babashka commands, and Library-Specific Guides table. Version bumped to 3.5.0.resources/conf/dev/config.edn: :boundary/ai-service added (Ollama primary, Anthropic fallback).resources/conf/test/config.edn: :boundary/ai-service {:provider :no-op} for test isolation.boundary-calendar — new library (Phase 2 / Q3 2026 roadmap)defevent macro and in-process registry (atom-backed, same pattern as defreport in boundary-reports): register named event type schemas at load time; get-event-type, list-event-types, clear-registry!.boundary.calendar.schema: Malli schemas — EventData, EventDef, OccurrenceResult, ConflictResult; helpers valid-event?, explain-event, valid-event-def?.boundary.calendar.core.event: pure helpers — duration, all-day?, within-range?.boundary.calendar.core.recurrence: DST-aware RRULE expansion via ical4j 4.x Recur with ZonedDateTime seeds; recurring?, occurrences, next-occurrence, expand-event.boundary.calendar.core.conflict: pairwise conflict detection — overlaps?, conflicts?, find-conflicts (returns ConflictResult maps with :overlap-start/:overlap-end).boundary.calendar.core.ui: pure Hiccup calendar views — event-badge, day-cell, month-view, week-view, mini-calendar.boundary.calendar.ports: CalendarAdapterProtocol (export-ical, import-ical).boundary.calendar.shell.adapters.ical: ICalAdapter backed by org.mnode.ical4j/ical4j 4.0.3; TZID extracted via regex from property text (ical4j 4.x creates synthetic zone IDs internally).boundary.calendar.shell.service: public API — export-ical, import-ical, ical-feed-response (returns Ring response with Content-Type: text/calendar; charset=utf-8).^:unit + ^:integration round-trip).libs/calendar/AGENTS.md: 11-section developer guide covering DST pitfalls, RRULE examples, ical4j 4.x API notes, registry pollution warning, REPL smoke check.docs-site/content/guides/calendar.adoc (weight 68): user-facing how-to guide.docs-site/content/api/calendar.adoc (weight 50): complete function API reference.dev-docs/adr/ADR-011-calendar-library.adoc: architecture decision record (7 decisions, alternatives considered).boundary-reports — added to CI (was missing)test-reports job added to .github/workflows/ci.yml; libs/reports/src added to the lint step..github/workflows/ci.yml: test-calendar and test-reports jobs added (both needs: lint; standalone, no inter-library dependencies). Both wired into test-summary.AGENTS.md and CLAUDE.md: reports and calendar added to library listing, test command reference, and Library-Specific Guides table. New "Adding a New Library to CI" checklist section in AGENTS.md.boundary-workflow — new library (Phase 2 / Q3 2026 roadmap)defworkflow macro and in-process registry: declare state machine definitions as data; get-workflow, list-workflows, clear-registry!.boundary.workflow.schema: Malli schemas — WorkflowDefinition, WorkflowInstance, TransitionDef, AuditEntry; state/transition validation at definition time.boundary.workflow.core.machine: pure state machine logic — can-transition?, find-transition, permission checks against :required-permissions, guard evaluation.boundary.workflow.core.transitions: available-transitions-with-status — returns all candidate transitions with :enabled?, :label, :reason for a given state and actor-roles.boundary.workflow.core.audit: pure audit entry constructors.boundary.workflow.ports: IWorkflowStore, IWorkflowEngine, IWorkflowRegistry protocols.boundary.workflow.shell.persistence: DB persistence via next.jdbc + HoneySQL (IWorkflowStore implementation).boundary.workflow.shell.service: orchestration — load → validate → persist → side-effects; create-workflow-service factory accepts optional job-queue and guard-registry.:context map; return boolean.TransitionDef (:side-effects [:notify-user]); enqueued via boundary-jobs after successful transition; silently skipped if no job queue configured.boundary.workflow.shell.http: REST API — POST /workflow/instances (start), POST /workflow/instances/:id/transition, GET /workflow/instances/:id (state + availableTransitions), GET /workflow/instances/:id/audit.boundary.workflow.shell.module-wiring: Integrant :boundary/workflow key; depends on :boundary/database-context (required) and :boundary/job-queue (optional).libs/workflow/AGENTS.md: developer guide covering defworkflow syntax, guards, side effects, auto-transitions, hooks, and Integrant wiring.docs-site/content/guides/workflow.adoc: user-facing how-to guide.boundary-workflow — lifecycle hooks, auto-transitions, available-transitions:hooks map on WorkflowDefinition: supports :on-enter-<state>, :on-exit-<state>, and :on-any-transition keys. Hooks receive the updated WorkflowInstance and fire synchronously after each successful transition (after the audit entry is saved). Exceptions are caught and logged; they do not roll back the transition.:auto? true on TransitionDef: marks a transition as system-initiated. process-auto-transitions! port method fires all eligible auto-transitions for a given workflow; uses [:system] actor-roles (no user permission check). Returns {:attempted :processed :failed} counts.available-transitions port method: returns candidate transitions with :enabled?, :label, and :reason fields for the current state and actor-roles. Exposed on the GET /api/workflow/instances/:id HTTP response as availableTransitions.:label on TransitionDef and :state-config map on WorkflowDefinition for human-readable display names.available-transitions-with-status pure function in boundary.workflow.core.transitions.boundary-search — filter support:filters key on SearchDefinition: declares filterable keyword dimensions (e.g. [:tenant-id :category-id]).:filter-values opt in index-document! and build-document: stores filter data as compact JSON in a new filters TEXT column.d.filters::jsonb->>'key' = ?; H2/SQLite uses INSTR(filters, '"key":"val"') > 0 (H2 2.4.x has no JDBC JSON function support).filter-key->json-key utility in boundary.search.core.index (kebab → snake conversion for JSON storage).resources/migrations/20260312000000-search-filters.{up,down}.sql.boundary-admin — Admin UI Frontend Redesign ("Refined Editorial")/fonts/ for CSP compliance (font-src 'self'); no external CDN dependency.fonts.css with variable-weight @font-face declarations (DM Sans 300–700, JetBrains Mono 400–600).boundary-tokens.css: --font-sans, --font-display, --font-mono.--shadow-sm through --shadow-2xl) for modern depth.--radius-sm 6px, --radius-md 8px, --radius-lg 12px, --radius-xl 16px).--transition-fast, --transition-normal, --transition-slow, --transition-bounce).--shadow-card-hover, --shadow-inner-glow, --tracking-tight, --tracking-tighter, --topbar-backdrop.backdrop-filter: blur(12px) saturate(180%).translateY(-2px)), icon color inversion on hover..entity-card-link, .entity-card-icon, .entity-card-title, .entity-card-count, .entity-card-description classes.translateY(-1px)).fadeInUp keyframe for page content entry with staggered delays.tableRowReveal keyframe for HTMX-loaded table rows (staggered first 10 rows).::after pseudo-element with scaleX transform.@media (prefers-reduced-motion: reduce) disables all animations.#0c0f17 base).rgba(255,255,255,0.08).border-radius: var(--radius-full).boundary-admin — UX Enhancements (6 features)htmx:beforeRequest / htmx:afterRequest / htmx:responseError events.HX-Trigger: {"showToast": {...}} response header or window.AdminUX.showToast() JS API..alert-success, .alert-error, etc.) auto-converted to toasts on page load.escapeHtml() prevents XSS in toast title/message content.data-href attribute).a, button, input, select, textarea, .actions-cell, .checkbox-cell, and td.editable are ignored (preserves inline editing).<thead>.w-full, w-3-4, w-1-2, w-1-3, w-1-4) for visual variety.window.confirm() for all delete operations.htmx:confirm event on elements with hx-delete or .danger class.data-confirm-title, data-confirm-cancel, data-confirm-label attributes (server-rendered via [:t ...] i18n markers); falls back to English.removeAfterAnimation() helper checks prefers-reduced-motion: reduce and removes DOM elements immediately instead of waiting for animationend (which never fires when animation: none).@media (prefers-reduced-motion: reduce).boundary-admin — Delete Flow ImprovementsHX-Redirect instead of empty response with HX-Trigger.return_to query parameter preserved through the delete flow for context-aware redirect.return_to validated to start with /web/admin/; invalid values fall back to entity list.boundary-admin — Pagination Enhancementspage-window algorithm improved: single-page gaps show the actual page number instead of an ellipsis (e.g. 1 2 3 ... 8 instead of 1 ... 3 ... 8 when page 2 is the only gap).[:t ...] markers (8 new i18n keys).boundary-i18n — New Translation Keysen.edn and nl.edn:
:admin/pagination-showing, :admin/pagination-of, :admin/pagination-label, :admin/pagination-first-page, :admin/pagination-previous-page, :admin/pagination-next-page, :admin/pagination-last-page, :admin/pagination-page.:admin/modal-button-cancel, :admin/modal-button-delete.boundary-admin — tenant entity + dashboard statsresources/conf/{dev,test}/admin/tenants.edn — list/search fields, status enum filter (active/suspended/deleted), field groups (Identity, State, Settings), readonly system fields.admin-home-handler now calls count-entities for each registered entity and passes the stats map to admin-home, so entity tiles show real counts instead of always displaying "0".#{:users :tenants}).boundary-tenant — convenience functions and protocol extensiontenant-provisioned? public function in boundary.tenant.shell.provisioning: checks if a tenant's schema exists in PostgreSQL; returns false for non-PostgreSQL databases; throws on missing :schema-name.list-tenant-schemas public function in boundary.tenant.shell.provisioning: lists all tenant_* schemas in PostgreSQL; returns empty vector for non-PostgreSQL databases.ITenantSchemaProvider protocol extended with tenant-provisioned? and list-tenant-schemas methods; TenantSchemaProvider record updated to implement both.dev-docs/adr/ADR-020-tenant-database-scope.adoc: decision to keep tenant provisioning PostgreSQL-only; MySQL/SQLite version promises removed from README.boundary-admin UI theme evolved from "Cyberpunk Professionalism" (Geist + Indigo/Lime) to "Refined Editorial" (DM Sans + JetBrains Mono, warmer surfaces, layered shadows, spring-eased transitions). Dark mode refined with blue-tinted surfaces and colored shadow glows.boundary-admin entity card markup restructured: icon standalone on its own line, then title, description, and count as metadata.boundary-admin delete handler: returns HX-Redirect header instead of empty body with HX-Trigger: entityDeleted.boundary-admin event listeners: all 7 document.body.addEventListener calls changed to document.addEventListener to survive HTMX body swaps (hx-target="body" hx-swap="outerHTML").boundary-ui-style CSS bundle: fonts.css added as first entry in admin-pilot-css; admin-ux.js added to admin-pilot-js.boundary-ui-style keyboard.js: confirm modal escape handling added; debug mode disabled.boundary-tenant promoted from "Active" to "Stable" in PROJECT_STATUS.adoc. All convenience functions documented in README are now implemented; 70 tests, 474 assertions, 0 failures.boundary-tenant README: fixed middleware naming (wrap-tenant-resolver → wrap-tenant-resolution), removed non-existent wrap-require-tenant (use :require-tenant? true option instead), clarified middleware locations (platform lib vs tenant lib), replaced MySQL/SQLite roadmap promises with ADR-020 reference.boundary-tenant integration tests: removed stale "DEFERRED" comment — tests pass with mock observability services and H2 in-memory DB.boundary-external promoted from "In Development" to "Active" (Twilio, SMTP/IMAP adapters production-capable). Stripe moved to boundary-payments.AGENTS.md updated: workflow and search added to library structure, test commands, and Library-Specific Guides table. Version bumped to 3.3.0.libs/workflow/AGENTS.md and libs/search/AGENTS.md updated to document all new features.docs-site/content/guides/workflow.adoc and docs-site/content/guides/search.adoc updated with new API examples, filter DDL, migration notes, and hook/auto-transition reference.libs/ui-style/resources/public/js/admin-ux.js — Central JS for all 5 UX features (~340 lines).libs/ui-style/resources/public/css/fonts.css — Self-hosted @font-face declarations.libs/ui-style/resources/public/fonts/dm-sans-latin.woff2 (63 KB).libs/ui-style/resources/public/fonts/dm-sans-italic-latin.woff2 (76 KB).libs/ui-style/resources/public/fonts/jetbrains-mono-latin.woff2 (31 KB).clojure -M:test:db/h2).clojure -M:test:db/h2 :admin).workflow.core.transitions-test (available-transitions-with-status), workflow.shell.service-test (hooks, auto-transitions), search.core.query-test (filter SQL), search.shell.persistence-test (filter round-trip).The first production-ready release of the Boundary Framework - a batteries-included web framework for Clojure that brings Django's productivity and Rails' conventions with functional programming rigor.
core/ namespaces (no side effects)shell/ namespacesports.clj for dependency injectionboundary-core (0.1.0)Foundation library with essential utilities:
boundary-observability (0.1.0)Multi-provider observability infrastructure:
boundary-platform (0.1.0)HTTP and database infrastructure:
boundary-user (0.1.0)Authentication and authorization:
boundary-admin (0.1.0)Auto-generated CRUD admin interface (Django Admin for Clojure):
deleted_at columnsboundary-storage (0.1.0)File storage abstraction:
boundary-scaffolder (0.1.0)Production-ready module generator:
boundary-cache (0.1.0)Distributed caching:
boundary-jobs (0.1.0)Background job processing:
run-at timestampboundary-realtime (0.1.0)WebSocket-based real-time communication:
boundary-tenant (0.1.0)Multi-tenancy infrastructure:
boundary-email (0.1.0)Email infrastructure:
boundary-external (0.1.0) - In DevelopmentExternal service adapters:
:field-order:field-groups/api/auth/mfa/setup, /api/auth/mfa/enable, /api/auth/mfa/verify:enter (request), :leave (response), :error (exception){:path "/api/admin"
:methods {:post {:handler 'handlers/create-resource
:interceptors ['auth/require-admin 'audit/log-action]
:summary "Create admin resource"}}}
(defn create-user [this user-data]
(service-interceptors/execute-service-operation
:create-user
{:user-data user-data}
(fn [{:keys [params]}]
;; Business logic here - observability automatic
(let [user (user-core/prepare-user (:user-data params))]
(.create-user repository user)))))
limit and offset parametersfirst, prev, next, last relationsdev, test, prod)#include support: Modular config files per moduleBND_ENVresources/conf/{env}/admin/{module}.edn:test alias)clojure -M:migrate uphttps://thijs-creemers.github.io/boundary/hugo server in docs-site/ directorydocs/cheatsheet.html with client-side search, copy-to-clipboard:password-hash, :created-atpassword_hash, created_atpasswordHash, createdAtsnake-case->kebab-case-map, kebab-case->snake-case-mapWhy: Recent bug caused authentication failures because service layer used :password_hash but entities had :password-hash. This convention prevents such mismatches.
:unit metadata):integration metadata):contract metadata)clojure -M:test:db/h2 # All tests
clojure -M:test:db/h2 :core # Core library
clojure -M:test:db/h2 --focus-meta :unit # Unit tests only
clojure -M:test:db/h2 --watch :core # Watch mode
clojure -M:repl-clj <<'EOF'
(require '[boundary.shared.tools.validation.repl :as v])
(spit "build/validation-user.dot" (v/rules->dot {:modules #{:user}}))
(System/exit 0)
EOF
dot -Tpng build/validation-user.dot -o docs/diagrams/validation-user.png
resources/public/css/tokens-openprops.css).github/workflows/publish.yml (304 lines)v*io.github.thijs-creemersthijs-creemers (password via GitHub Secrets)boundary-core → io.github.thijs-creemers/boundary-coreboundary-observability → io.github.thijs-creemers/boundary-observabilityboundary-platform → io.github.thijs-creemers/boundary-platformboundary-user → io.github.thijs-creemers/boundary-userboundary-admin → io.github.thijs-creemers/boundary-adminboundary-storage → io.github.thijs-creemers/boundary-storageboundary-scaffolder → io.github.thijs-creemers/boundary-scaffolderboundary-cache → io.github.thijs-creemers/boundary-cacheboundary-jobs → io.github.thijs-creemers/boundary-jobsboundary-tenant → io.github.thijs-creemers/boundary-tenantboundary-email → io.github.thijs-creemers/boundary-emailboundary-external → io.github.thijs-creemers/boundary-external (skeleton, not production-ready)Use the boundary-starter template:
git clone https://github.com/thijs-creemers/boundary-starter
cd boundary-starter
export JWT_SECRET="change-me-dev-secret-min-32-chars"
export BND_ENV="development"
clojure -M:repl-clj
In REPL:
(require '[integrant.repl :as ig-repl])
(ig-repl/go) ;; Visit http://localhost:3000
What you get:
;; deps.edn
{:deps {io.github.thijs-creemers/boundary-core {:mvn/version "1.0.0"}
io.github.thijs-creemers/boundary-platform {:mvn/version "1.0.0"}
io.github.thijs-creemers/boundary-user {:mvn/version "1.0.0"}
io.github.thijs-creemers/boundary-admin {:mvn/version "1.0.0"}}}
clojure -T:build clean && clojure -T:build uber
java -jar target/boundary-*.jar server
Use provided Dockerfile in boundary-starter template.
export JWT_SECRET="production-secret-min-32-chars"
export BND_ENV="production"
export DB_PASSWORD="secure_password"
export DATABASE_URL="jdbc:postgresql://localhost:5432/boundary"
tx (15 occurrences)tx-ctx (5 occurrences)These are false positives from clj-kondo's static analysis and do not affect runtime behavior.
let expressions: 3 warnings in test files (cosmetic issue)This is the initial 1.0.0 release. No migration from previous versions.
Copyright 2024-2025 Thijs Creemers.
Distributed under the Eclipse Public License 2.0.
boundary new bb.edn template — full boundary-tools task suite, version re-alignmentCan you improve this documentation? These fine people already did:
Thijs Creemers & thijscreemersEdit 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 |