platform
Infrastructure layer for HTTP routing/interceptors, database integration, CLI/runtime wiring, and shared platform adapters used by feature libraries.
Key namespaces
| Namespace | Purpose |
|---|---|
|
HTTP interceptor composition and execution |
|
Normalized route handling and dispatch |
|
Database setup, context, and shared persistence infrastructure |
HTTP interceptors
Declarative cross-cutting concerns (auth, rate limiting, audit):
;; Define an interceptor
(def require-admin
{:name :require-admin
:enter (fn [ctx]
(if (admin? (get-in ctx [:request :session :user]))
ctx
;; To short-circuit you MUST set :halt? true — setting :response
;; alone does not stop the pipeline, so the downstream handler
;; would run and overwrite this response. See "Interceptor phases".
(assoc ctx :halt? true
:response {:status 403 :body {:error "Forbidden"}})))
:leave (fn [ctx] ctx) ; optional response processing
:error (fn [ctx err] ; optional error handling
(assoc ctx :response {:status 500 :body {:error "Internal error"}}))})
;; Attach interceptors to routes
[{:path "/api/admin"
:methods {:post {:handler 'handlers/create-resource
:interceptors ['auth/require-admin
'audit/log-action]
:summary "Create admin resource"}}}]
Built-in interceptors
The default HTTP stack (boundary.platform.shell.http.interceptors/default-http-interceptors)
is applied to every route unless the route opts out with :skip-interceptors? true
(used only for internal endpoints such as health checks). In :enter/:leave order:
-
http-request-logging— request entry/completion logging with timing -
http-request-metrics— timing and status-code metrics -
http-error-reporting— captures exceptions to the error-tracking service -
http-correlation-header— addsX-Correlation-IDto the response -
http-csrf-protection— validates/issues CSRF tokens (see CSRF protection) -
http-security-headers— CSP, HSTS,X-Frame-Options,X-Content-Type-Options, … -
http-error-handler— maps an exception’s:typeto an HTTP status
http-rate-limit (fixed-window, optionally Redis-backed) is available to attach per route.
|
Note
|
:skip-interceptors? is independent of :no-doc. :no-doc only excludes a
route from the Swagger spec; it does not affect interceptors. All /web routes are
:no-doc yet still run the full stack so that security interceptors apply to the UI.
|
Interceptor phases
-
:enter— request processing (auth, validation, transformation) -
:leave— response processing (audit, metrics, transformation) -
:error— exception handling (custom error responses)
Enter phases run in order; leave phases run in reverse order.
To reject a request from :enter, set :halt? true in the context (alongside the
:response). The pipeline short-circuits only on :halt? — setting :response by
itself does not stop forward execution, so a later interceptor or the route handler
would overwrite it. :leave still runs for interceptors that already executed.
CSRF protection
http-csrf-protection enforces CSRF for session-authenticated, state-changing
requests and issues tokens for rendering. Pure token functions live in
boundary.platform.core.csrf.
A state-changing request (POST/PUT/DELETE/PATCH) is validated — 403 on a missing or
invalid token — when CSRF is enabled, the path is not exempt, and the request is either
session-authenticated (session-token cookie / X-Session-Token header) or a /web
route. This covers /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; safe methods (GET/HEAD/OPTIONS) and :exempt-paths are skipped.
;; resources/conf/<env>/config.edn under :active
:boundary/http
{:security
{:csrf {:enabled? true ; OPT-IN: lib default is false
:secret #or [#env CSRF_SECRET #env JWT_SECRET] ; defaults to JWT_SECRET
:exempt-paths ["/api/v1/payments/webhook"]}}} ; trailing /* = segment-prefix
Enforcement is opt-in: the library default is :enabled? false, so upgrading the
framework cannot start rejecting requests from a consumer that does not yet emit
tokens. An app turns it on with the block above (after emitting tokens in its /web
forms); the secret falls back to JWT_SECRET. Startup fails loud — the system wiring
throws and the app refuses to boot if CSRF is enabled with a blank secret, rather than
letting the interceptor fail open (run unvalidated).
Tokens are emitted with no per-handler wiring. For HTMX, either merge
(csrf/hx-headers) onto an element’s attributes (e.g. <body>) so all inherited
hx-* requests carry the X-CSRF-Token header, or rely on the shared page layout’s
<meta name="csrf-token"> tag plus the global htmx:configRequest listener that
attaches the header to every HTMX request. Plain (non-HTMX) forms include a hidden
field:
(require '[boundary.platform.core.csrf :as csrf])
[:form {:method "post" :action "/web/logout"}
(csrf/hidden-field) ; reads the token bound for the current request
[:button "Logout"]]
See the authentication guide for the request flow and the unauthenticated (login) pre-session token.
Testing
clojure -M:test:db/h2 :platform