The permission registry, built from what modules declare (ADR-041 §1–2).
A module contributes under :authorization:
{:permissions {:orders/refund {:label "Refund orders"}} :roles {:orders/viewer {:description "…" :permissions #{:orders/read}}} :scopes {:domain-id {:label "Domain" :entity :domains}} :scope-sources {:user/department {:label "The member's department" :resolve (fn [user] #{7})}}}
<module>/read and <module>/write exist for every module without being
declared, and so do the code roles <module>/viewer and <module>/editor
holding them, unless the module declares its own. What a scope means is
ADR-041 §4, core.scope.
The permission registry, built from what modules declare (ADR-041 §1–2).
A module contributes under `:authorization`:
{:permissions {:orders/refund {:label "Refund orders"}}
:roles {:orders/viewer {:description "…" :permissions #{:orders/read}}}
:scopes {:domain-id {:label "Domain" :entity :domains}}
:scope-sources {:user/department {:label "The member's department"
:resolve (fn [user] #{7})}}}
`<module>/read` and `<module>/write` exist for every module without being
declared, and so do the code roles `<module>/viewer` and `<module>/editor`
holding them, unless the module declares its own. What a scope means is
ADR-041 §4, `core.scope`.When to stop calling a service that keeps failing.
Retries bound the damage of one call; this bounds the damage of many. A service that is down still receives every request until each one times out, and every caller pays that timeout. Declining to call is the only thing that stops both.
FC/IS: pure. The state lives elsewhere — see …shell.breaker, which
keeps it in the cache so replicas share one breaker rather than each
discovering the outage separately.
When to stop calling a service that keeps failing. Retries bound the damage of one call; this bounds the damage of many. A service that is down still receives every request until each one times out, and every caller pays that timeout. Declining to call is the only thing that stops both. FC/IS: pure. The state lives elsewhere — see `…shell.breaker`, which keeps it in the cache so replicas share one breaker rather than each discovering the outage separately.
Pure CSRF token functions — synchronizer token bound to a session.
The token is a signed double-submit value:
token = base64url(nonce) "." base64url(HMAC-SHA256(secret, nonce || binding))
The binding is the value the token is tied to: the user's session token for
authenticated requests, or a per-request pre-session cookie value for the login
form (which has no session yet). A token is only valid when presented together
with the same binding it was signed against, which is what defeats CSRF: an
attacker can forge a cross-site request but cannot read the victim's binding to
produce a matching token.
Functional Core: these functions are pure and deterministic. The CSPRNG nonce and the secret are produced in the shell and passed in as arguments — nothing here performs I/O or reads ambient state.
Safety parity with ring-anti-forgery: validation uses buddy's mac/verify,
which performs a constant-time comparison of the recomputed HMAC, so token
checks do not leak timing information.
Enforcement is opt-in at the interceptor level (default off); see
wagoe.platform.shell.http.interceptors/http-csrf-protection. Emit the token
with hidden-field (server forms) or hx-headers (HTMX elements), or via the
<meta name="csrf-token"> tag + the ui-style init.js htmx:configRequest listener.
Pure CSRF token functions — synchronizer token bound to a session.
The token is a signed double-submit value:
token = base64url(nonce) "." base64url(HMAC-SHA256(secret, nonce || binding))
The `binding` is the value the token is tied to: the user's session token for
authenticated requests, or a per-request pre-session cookie value for the login
form (which has no session yet). A token is only valid when presented together
with the same binding it was signed against, which is what defeats CSRF: an
attacker can forge a cross-site request but cannot read the victim's binding to
produce a matching token.
Functional Core: these functions are pure and deterministic. The CSPRNG nonce
and the secret are produced in the shell and passed in as arguments — nothing
here performs I/O or reads ambient state.
Safety parity with ring-anti-forgery: validation uses buddy's `mac/verify`,
which performs a constant-time comparison of the recomputed HMAC, so token
checks do not leak timing information.
Enforcement is opt-in at the interceptor level (default off); see
`wagoe.platform.shell.http.interceptors/http-csrf-protection`. Emit the token
with `hidden-field` (server forms) or `hx-headers` (HTMX elements), or via the
<meta name="csrf-token"> tag + the ui-style init.js htmx:configRequest listener.Who may reach a route, and what a refused caller is told.
Every route requires a signed-in user unless its route data carries
:public true (BOU-568). A refusal has the shape the user module's
require-authenticated interceptor answers with, so a client sees one 401
whichever layer refused it: the one error body of
wagoe.platform.core.http.errors (BOU-586). No Content-Type: muuntaja encodes a map body
only when none is set (ZZP-120). What needs no HTTP stack lives in
wagoe.core.http.access (BOU-930).
Who may reach a route, and what a refused caller is told. Every route requires a signed-in user unless its route data carries `:public true` (BOU-568). A refusal has the shape the user module's `require-authenticated` interceptor answers with, so a client sees one 401 whichever layer refused it: the one error body of `wagoe.platform.core.http.errors` (BOU-586). No Content-Type: muuntaja encodes a map body only when none is set (ZZP-120). What needs no HTTP stack lives in `wagoe.core.http.access` (BOU-930).
The address a request came from, as opposed to the proxy that delivered it.
:remote-addr is the peer, which behind a reverse proxy is the proxy. Any
client can send X-Forwarded-For, so it is read only when the peer is a
trusted proxy, and from the right: each proxy appends the address it saw,
so the first hop that is not one of ours is the client.
The address a request came from, as opposed to the proxy that delivered it. `:remote-addr` is the peer, which behind a reverse proxy is the proxy. Any client can send X-Forwarded-For, so it is read only when the peer is a trusted proxy, and from the right: each proxy appends the address it saw, so the first hop that is not one of ours is the client.
What a browser is shown for an error body: a page for a /web request, a
toast for an htmx one. An API caller, or a script asking for JSON, keeps the
body of wagoe.platform.core.http.errors.
What a browser is shown for an error body: a page for a /web request, a toast for an htmx one. An API caller, or a script asking for JSON, keeps the body of `wagoe.platform.core.http.errors`.
The one JSON error body (BOU-586):
{"error": {"type": "not-found", "message": "User not found", "details": {...}, ; optional "correlation-id": "...", ; optional "dev": {...}}} ; optional, dev only (WGE code)
Everything about the failure sits inside error, so a client reads one
key. type is a string: the :type keyword of the ex-info, without its
colon. The pure part lives in wagoe.core.http.errors (BOU-930); this
namespace adds what needs JSON.
The one JSON error body (BOU-586):
{"error": {"type": "not-found",
"message": "User not found",
"details": {...}, ; optional
"correlation-id": "...", ; optional
"dev": {...}}} ; optional, dev only (WGE code)
Everything about the failure sits inside `error`, so a client reads one
key. `type` is a string: the `:type` keyword of the ex-info, without its
colon. The pure part lives in `wagoe.core.http.errors` (BOU-930); this
namespace adds what needs JSON.The sidebar navigation modules contribute, as data.
A module's route contribution may carry :nav, groups by id:
{:user {:label [:t :user/nav-title] :icon :users :entities [:users] ; admin entities this module owns :items [{:href "/web/audit" :label .. :icon :file-text :access :platform-admin :description ..}]}} ; optional, for dashboard cards
An item with :entity (the admin contributes one per entity) is placed by
module: its :module, else the group whose :entities names it, else
:other. Other items stay in the group that declared them.
The platform binds *nav* around each request, so a page renders the whole
sidebar without depending on the modules in it.
The sidebar navigation modules contribute, as data.
A module's route contribution may carry `:nav`, groups by id:
{:user {:label [:t :user/nav-title]
:icon :users
:entities [:users] ; admin entities this module owns
:items [{:href "/web/audit" :label .. :icon :file-text
:access :platform-admin
:description ..}]}} ; optional, for dashboard cards
An item with `:entity` (the admin contributes one per entity) is placed by
module: its `:module`, else the group whose `:entities` names it, else
`:other`. Other items stay in the group that declared them.
The platform binds `*nav*` around each request, so a page renders the whole
sidebar without depending on the modules in it.Whether a page is mounted, for layouts deciding what to link to.
Whether a page is mounted, for layouts deciding what to link to.
Exceptions to error responses, and the :type -> status table.
The name is historical: these were RFC 7807 bodies until BOU-586 made every
JSON error the one shape of wagoe.platform.core.http.errors.
Exceptions to error responses, and the `:type` -> status table. The name is historical: these were RFC 7807 bodies until BOU-586 made every JSON error the one shape of `wagoe.platform.core.http.errors`.
Pure functions for pagination logic.
This namespace provides pure functional implementations for pagination calculations, following the Functional Core pattern. All functions are deterministic and side-effect free.
Supports:
Pure: All functions return data, no side effects.
Pure functions for pagination logic. This namespace provides pure functional implementations for pagination calculations, following the Functional Core pattern. All functions are deterministic and side-effect free. Supports: - Offset-based pagination (simple, familiar) - Cursor-based pagination (high performance, stable results) - Parameter validation - Metadata calculation Pure: All functions return data, no side effects.
Pure functions for API versioning logic.
This namespace provides pure functional implementations for API version management, following the Functional Core pattern. All functions are deterministic and side-effect free.
Supports:
Pure: All functions return data, no side effects.
Pure functions for API versioning logic. This namespace provides pure functional implementations for API version management, following the Functional Core pattern. All functions are deterministic and side-effect free. Supports: - Version parsing and comparison - Version lifecycle management (experimental → stable → deprecated → sunset) - Version validation - Migration path tracking Pure: All functions return data, no side effects.
Pure halves of data export and retention: the export document, and which retention policies run with which cutoff.
Pure halves of data export and retention: the export document, and which retention policies run with which cutoff.
How long to wait between attempts, and how long a whole run of them can take.
Shared by the RPC client and the libs/external adapters, so a flaky third-party API and a flaky internal service get one policy. Which failures are worth retrying is the caller's call; it knows what a second attempt could repeat.
FC/IS: pure.
How long to wait between attempts, and how long a whole run of them can take. Shared by the RPC client and the libs/external adapters, so a flaky third-party API and a flaky internal service get one policy. Which failures are worth retrying is the caller's call; it knows what a second attempt could repeat. FC/IS: pure.
Pure RPC envelope handling for the remote-port adapter.
A cross-module call goes through a protocol (ports.clj). Slicing a module
into its own process means implementing that same protocol with something
that makes a network call instead — the seam is already there, only the
adapter is missing (BOU-90, scaling.adoc → Functional decomposition).
This namespace is the wire contract and nothing else: building an envelope,
reading one, and turning a remote failure into the error shape callers
already handle. No I/O — see wagoe.platform.shell.rpc.client and
…rpc.server for that.
FC/IS: pure. Everything here is data in, data out.
Pure RPC envelope handling for the remote-port adapter. A cross-module call goes through a protocol (`ports.clj`). Slicing a module into its own process means implementing that same protocol with something that makes a network call instead — the seam is already there, only the adapter is missing (BOU-90, scaling.adoc → Functional decomposition). This namespace is the wire contract and nothing else: building an envelope, reading one, and turning a remote failure into the error shape callers already handle. No I/O — see `wagoe.platform.shell.rpc.client` and `…rpc.server` for that. FC/IS: pure. Everything here is data in, data out.
Scopes on rights (ADR-041 §4).
The user module hands over a user's rights as
[{:permission kw :scope nil-or-{key value}}], each value fixed or a
:user/<name> source. Resolved (shell.authorization/resolve-rights), a
scope is {key #{value}}, and {} holds everywhere. Rights add up: a
permission is held in each of its scopes.
Safe by default: a scope admits only where every one of its keys is checked.
Scopes on rights (ADR-041 §4).
The user module hands over a user's rights as
`[{:permission kw :scope nil-or-{key value}}]`, each value fixed or a
`:user/<name>` source. Resolved (`shell.authorization/resolve-rights`), a
scope is `{key #{value}}`, and `{}` holds everywhere. Rights add up: a
permission is held in each of its scopes.
Safe by default: a scope admits only where every one of its keys is checked.Cut an Integrant config down to the modules one service runs.
Modules are already gated by :enabled?, and the router mounts only the
routes it is handed, so a process can run a subset — there was just no way
to say which subset (BOU-91). This is that, as a pure transformation:
config in, smaller config out.
The whole difficulty is refs. Dropping :wagoe/tenant-service leaves
:wagoe/http-handler pointing at a key that no longer exists, and Integrant
refuses to build a config with a dangling ref — so the refs have to go with
the keys, and they have to go first, or the closure that works out what is
still needed follows them straight back to everything.
FC/IS: pure. Nothing here starts, stops or reads anything.
Cut an Integrant config down to the modules one service runs. Modules are already gated by `:enabled?`, and the router mounts only the routes it is handed, so a process *can* run a subset — there was just no way to say which subset (BOU-91). This is that, as a pure transformation: config in, smaller config out. The whole difficulty is refs. Dropping `:wagoe/tenant-service` leaves `:wagoe/http-handler` pointing at a key that no longer exists, and Integrant refuses to build a config with a dangling ref — so the refs have to go with the keys, and they have to go *first*, or the closure that works out what is still needed follows them straight back to everything. FC/IS: pure. Nothing here starts, stops or reads anything.
The permission registry: every permission, code role and scope key the
enabled modules declare or derive. Built once at boot as
:wagoe/permission-registry; nothing outside it can be granted.
The permission registry: every permission, code role and scope key the enabled modules declare or derive. Built once at boot as `:wagoe/permission-registry`; nothing outside it can be granted.
The HTTP server port.
Routing is not a port: modules emit Reitit route data and the platform compiles it (ADR-037). The server is — a Ring handler is handed to whatever implements this protocol.
The HTTP server port. Routing is not a port: modules emit Reitit route data and the platform compiles it (ADR-037). The server is — a Ring handler is handed to whatever implements this protocol.
Retry and circuit breaker for outbound calls — the RPC client and the libs/external adapters both go through here.
Outside shell/ for the reason wagoe.platform.database is: other modules
call it, and reaching into platform's shell is what bb check:ports refuses.
Both take predicates over a result rather than deciding what counts as a failure. Only the caller knows whether a second attempt could run something twice.
Retry and circuit breaker for outbound calls — the RPC client and the libs/external adapters both go through here. Outside `shell/` for the reason `wagoe.platform.database` is: other modules call it, and reaching into platform's shell is what `bb check:ports` refuses. Both take predicates over a result rather than deciding what counts as a failure. Only the caller knows whether a second attempt could run something twice.
Malli schemas for platform-level concerns: pagination, versioning, API metadata.
These schemas are used across all modules for consistent API responses.
Malli schemas for platform-level concerns: pagination, versioning, API metadata. These schemas are used across all modules for consistent API responses.
No vars found in this namespace.
No vars found in this namespace.
No vars found in this namespace.
No vars found in this namespace.
File system adapter implementation for local filesystem operations.
Provides concrete implementation of IFileSystemAdapter protocol using Java I/O for local filesystem access.
File system adapter implementation for local filesystem operations. Provides concrete implementation of IFileSystemAdapter protocol using Java I/O for local filesystem access.
Protocol defining the common interface for filesystem adapters.
This protocol abstracts filesystem operations to enable testing with in-memory implementations and support different storage backends (local FS, cloud storage, etc.).
Design Philosophy:
Protocol defining the common interface for filesystem adapters. This protocol abstracts filesystem operations to enable testing with in-memory implementations and support different storage backends (local FS, cloud storage, etc.). Design Philosophy: - Protocol defines generic file operations - Implementations handle specific storage backends - Enables testing without actual filesystem I/O - Supports multiple storage strategies (local, cloud, in-memory)
No vars found in this namespace.
:wagoe/permission-registry: the registry core.authorization builds,
behind IPermissionRegistry, the scope check of a route (ADR-041 §4) and
the authorization interceptor that runs both (§5).
Read it at the REPL with
(ports/permissions (get (wagoe.platform.shell.system.wiring/system) :wagoe/permission-registry))
`:wagoe/permission-registry`: the registry `core.authorization` builds,
behind `IPermissionRegistry`, the scope check of a route (ADR-041 §4) and
the authorization interceptor that runs both (§5).
Read it at the REPL with
(ports/permissions (get (wagoe.platform.shell.system.wiring/system)
:wagoe/permission-registry))Circuit-breaker state, kept where every replica can see it.
A breaker in one JVM's atom protects that JVM. With N replicas a failing service gets N breakers, each of which has to trip on its own, so it still takes N times the load — and when the window elapses, all N probe at once, which is the stampede the breaker was meant to prevent. Keeping the state in the cache port makes it one breaker.
The cache adapter decides how far that goes: Redis shares it across replicas, the in-memory one does not. That is the same trade the rate limiter makes, and it is a property of the adapter rather than of this namespace.
circuit names what is being protected: the RPC client passes rpc: plus
the service URL, the external adapters their kind and host.
Circuit-breaker state, kept where every replica can see it. A breaker in one JVM's atom protects that JVM. With N replicas a failing service gets N breakers, each of which has to trip on its own, so it still takes N times the load — and when the window elapses, all N probe at once, which is the stampede the breaker was meant to prevent. Keeping the state in the cache port makes it one breaker. The cache adapter decides how far that goes: Redis shares it across replicas, the in-memory one does not. That is the same trade the rate limiter makes, and it is a property of the adapter rather than of this namespace. `circuit` names what is being protected: the RPC client passes `rpc:` plus the service URL, the external adapters their kind and host.
Reading feature flags from the process environment.
wagoe.core.config.feature-flags decides what a flag means, given an
environment map. This namespace is the one line that fetches the map, and it
lives in a shell because reading System/getenv is I/O — ambient input that
changes the answer without changing the arguments.
Both halves used to be in the core namespace, whose docstring promised no
side effects while five of its functions called (System/getenv) in a
convenience arity (BOU-302).
Reading feature flags from the process environment. `wagoe.core.config.feature-flags` decides what a flag means, given an environment map. This namespace is the one line that fetches the map, and it lives in a shell because reading `System/getenv` is I/O — ambient input that changes the answer without changing the arguments. Both halves used to be in the core namespace, whose docstring promised no side effects while five of its functions called `(System/getenv)` in a convenience arity (BOU-302).
Ring flash in a cookie, not the Ring session: :session is the user
module's database session. Unsigned, since only toast text is read back.
Ring flash in a cookie, not the Ring session: `:session` is the user module's database session. Unsigned, since only toast text is read back.
HTTP interceptor pipeline for Ring request/response processing.
This namespace provides an HTTP-specific interceptor chain that runs over Ring handlers, giving Pedestal-like enter/leave/error semantics while maintaining Ring compatibility.
Key Benefits:
HTTP Context Model: {:request Ring request map :response Ring response map (built up through pipeline) :route Route metadata from Reitit match :path-params Extracted path parameters :query-params Extracted query parameters :system Observability services {:logger :metrics-emitter :error-reporter} :attrs Additional attributes set by interceptors :correlation-id Unique request ID :started-at Request start timestamp}
Interceptor Shape: {:name :my-interceptor :enter (fn [context] ...) ; Process request, modify context :leave (fn [context] ...) ; Process response, modify context :error (fn [context] ...)} ; Handle exceptions, produce safe response
Usage: ;; As Ring middleware (global) (def handler (-> app-handler (wrap-http-interceptors [logging metrics error-reporting])))
;; Per-route via Reitit :middleware {:get {:handler my-handler :middleware [(interceptor-middleware [auth rate-limit])]}}
Route data may also carry an :interceptors vector; when the routes are compiled, it becomes :middleware running through this same runner.
HTTP interceptor pipeline for Ring request/response processing.
This namespace provides an HTTP-specific interceptor chain that runs over Ring handlers,
giving Pedestal-like enter/leave/error semantics while maintaining Ring compatibility.
Key Benefits:
- Consistent cross-layer observability (HTTP → service → persistence)
- Declarative per-route policies (auth, rate-limit, auditing)
- Clean separation of concerns (routing vs cross-cutting logic)
- Reusable interceptor components across routes
HTTP Context Model:
{:request Ring request map
:response Ring response map (built up through pipeline)
:route Route metadata from Reitit match
:path-params Extracted path parameters
:query-params Extracted query parameters
:system Observability services {:logger :metrics-emitter :error-reporter}
:attrs Additional attributes set by interceptors
:correlation-id Unique request ID
:started-at Request start timestamp}
Interceptor Shape:
{:name :my-interceptor
:enter (fn [context] ...) ; Process request, modify context
:leave (fn [context] ...) ; Process response, modify context
:error (fn [context] ...)} ; Handle exceptions, produce safe response
Usage:
;; As Ring middleware (global)
(def handler
(-> app-handler
(wrap-http-interceptors [logging metrics error-reporting])))
;; Per-route via Reitit :middleware
{:get {:handler my-handler
:middleware [(interceptor-middleware [auth rate-limit])]}}
Route data may also carry an :interceptors vector; when the routes are
compiled, it becomes :middleware running through this same runner.Asks the request's router which pages are mounted.
Asks the request's router which pages are mounted.
Compiles the application's route data into a Ring handler.
Modules emit Reitit route data and this namespace hands it to Reitit (ADR-037), so what is left here is behaviour rather than translation: exception handling, the coercion error shape, the OpenAPI routes, gzip, and the interceptor stack each route is decorated with.
Compiles the application's route data into a Ring handler. Modules emit Reitit route data and this namespace hands it to Reitit (ADR-037), so what is left here is behaviour rather than translation: exception handling, the coercion error shape, the OpenAPI routes, gzip, and the interceptor stack each route is decorated with.
Ring+Jetty server adapter - manages HTTP server lifecycle using Ring and Jetty.
This adapter implements the IHttpServer protocol to start and stop Jetty-based HTTP servers with Ring handlers.
Ring+Jetty server adapter - manages HTTP server lifecycle using Ring and Jetty. This adapter implements the IHttpServer protocol to start and stop Jetty-based HTTP servers with Ring handlers.
HTTP API versioning support - wraps routes with version prefixes and headers.
SIDE EFFECTS:
Provides URL-based versioning (/api/v1/users, /api/v2/users) with:
HTTP API versioning support - wraps routes with version prefixes and headers. SIDE EFFECTS: - Route transformation - Response header modification - Logging Provides URL-based versioning (/api/v1/users, /api/v2/users) with: - Automatic version prefix wrapping - Version header injection (X-API-Version, X-API-Latest, X-API-Deprecated) - Backward compatibility (/api/users → /api/v1/users redirect) - Multiple version support concurrently
Universal interceptors for cross-cutting concerns.
These interceptors handle common infrastructure concerns like logging, metrics, error handling, and context management across all modules.
Universal interceptors for cross-cutting concerns. These interceptors handle common infrastructure concerns like logging, metrics, error handling, and context management across all modules.
No vars found in this namespace.
No vars found in this namespace.
CLI middleware for command execution with enhanced error context (imperative shell).
This namespace provides reusable CLI middleware that can be used across different modules and applications for consistent observability and error handling in command-line interfaces.
Middleware functions handle CLI boundaries: command execution interception, context generation, logging, and exception handling with enhanced context preservation for debugging.
Features:
CLI middleware for command execution with enhanced error context (imperative shell). This namespace provides reusable CLI middleware that can be used across different modules and applications for consistent observability and error handling in command-line interfaces. Middleware functions handle CLI boundaries: command execution interception, context generation, logging, and exception handling with enhanced context preservation for debugging. Features: - Command correlation ID generation - Tenant and user context extraction from config/environment - Structured command execution logging with observability context - Command timing and error reporting with enhanced context - Error reporting breadcrumb integration
No vars found in this namespace.
Common HTTP utilities: error responses in the one error shape
(wagoe.platform.core.http.errors) and health checks.
The pure transformations are in wagoe.platform.core.http.problem-details.
Common HTTP utilities: error responses in the one error shape (`wagoe.platform.core.http.errors`) and health checks. The pure transformations are in wagoe.platform.core.http.problem-details.
No vars found in this namespace.
No vars found in this namespace.
Module registry and composition helpers for the Wagoe shell.
This namespace provides utilities to:
For now, only the :user module is supported.
Module registry and composition helpers for the Wagoe shell. This namespace provides utilities to: - Determine which modules are enabled based on configuration. - Compose route definitions from multiple modules. - Dispatch CLI commands to module-specific runners. For now, only the `:user` module is supported.
Shell layer cursor encoding/decoding for cursor-based pagination.
SIDE EFFECTS:
Cursors are opaque tokens that encode:
Format: Base64(JSON({:id ... :sort-value ... :sort-field ... :sort-direction ...}))
Shell layer cursor encoding/decoding for cursor-based pagination.
SIDE EFFECTS:
- JSON encoding/decoding
- Base64 encoding/decoding
- Exception throwing on invalid cursors
Cursors are opaque tokens that encode:
- Item ID (for stable ordering)
- Sort field value (for comparison)
- Sort direction (asc/desc)
- Optional timestamp (for expiry)
Format: Base64(JSON({:id ... :sort-value ... :sort-field ... :sort-direction ...}))Shell layer RFC 5988 Link header generation for pagination.
RFC 5988: Web Linking https://datatracker.ietf.org/doc/html/rfc5988
SIDE EFFECTS:
Link headers provide hypermedia controls for pagination navigation. They allow clients to discover next, prev, first, and last pages without parsing response bodies.
Format: Link: </api/v1/users?limit=20&offset=20>; rel="next", </api/v1/users?limit=20&offset=0>; rel="first", </api/v1/users?limit=20&offset=980>; rel="last"
Supported Relations:
Shell layer RFC 5988 Link header generation for pagination.
RFC 5988: Web Linking
https://datatracker.ietf.org/doc/html/rfc5988
SIDE EFFECTS:
- String building and URL encoding
- Logging
Link headers provide hypermedia controls for pagination navigation.
They allow clients to discover next, prev, first, and last pages without
parsing response bodies.
Format:
Link: </api/v1/users?limit=20&offset=20>; rel="next",
</api/v1/users?limit=20&offset=0>; rel="first",
</api/v1/users?limit=20&offset=980>; rel="last"
Supported Relations:
- first: First page of results
- last: Last page of results (offset pagination only)
- prev: Previous page
- next: Next page
- self: Current pagePersistence-level interceptors for database operations.
This namespace provides interceptor pipelines specifically designed for persistence layer operations, eliminating manual observability calls and providing consistent error handling, logging, and error reporting across all database operations.
Key Benefits:
Usage: (execute-persistence-operation :find-user-by-id {:user-id user-id} db-impl-fn {:system system})
Architecture:
Persistence-level interceptors for database operations.
This namespace provides interceptor pipelines specifically designed for persistence layer operations,
eliminating manual observability calls and providing consistent error handling, logging, and
error reporting across all database operations.
Key Benefits:
- Eliminates 40+ manual observability calls from persistence methods
- Provides consistent database error handling patterns
- Automatic timing and logging for all database operations
- Single point of control for persistence-level cross-cutting concerns
Usage:
(execute-persistence-operation
:find-user-by-id
{:user-id user-id}
db-impl-fn
{:system system})
Architecture:
- Persistence interceptors wrap database operations
- Context carries operation metadata and database context
- Interceptors handle all database-related cross-cutting concerns automaticallyData export and retention over what modules contribute: :data-export
exporters and :retention policies (wagoe.platform.ports.privacy).
Retention is off until :wagoe/privacy :retention names a policy, and then
runs as the recurring job retention-job-type on :wagoe/jobs:
:wagoe/privacy {:retention {:user/audit-log {:retain "P2Y"}} :retention-schedule {:cron "0 3 * * *" :zone "UTC"}}
Data export and retention over what modules contribute: `:data-export`
exporters and `:retention` policies (`wagoe.platform.ports.privacy`).
Retention is off until `:wagoe/privacy :retention` names a policy, and then
runs as the recurring job `retention-job-type` on `:wagoe/jobs`:
:wagoe/privacy {:retention {:user/audit-log {:retain "P2Y"}}
:retention-schedule {:cron "0 3 * * *" :zone "UTC"}}The throwables already sent to the error tracker, so a 5xx that crosses a service and then the HTTP layer is reported once (BOU-714). Keys are weak: remembering a throwable does not keep it alive.
The throwables already sent to the error tracker, so a 5xx that crosses a service and then the HTTP layer is reported once (BOU-714). Keys are weak: remembering a throwable does not keep it alive.
Generic remote-port adapter: implement any module protocol over HTTP.
Cross-module calls go through a protocol, so slicing a module into its own
process needs one thing the codebase did not have — an implementation of
that protocol that makes a network call. This builds one for an arbitrary
protocol, so the caller keeps calling (ports/create-checkout-session svc …)
and does not learn that the answer now comes over a socket (BOU-90).
Modelled on libs/external's outbound adapters: clj-http, :throw-exceptions false, errors as data rather than exceptions.
FC/IS: shell. The wire contract is pure and lives in
wagoe.platform.core.rpc.
Generic remote-port adapter: implement any module protocol over HTTP. Cross-module calls go through a protocol, so slicing a module into its own process needs one thing the codebase did not have — an implementation of that protocol that makes a network call. This builds one for an arbitrary protocol, so the caller keeps calling `(ports/create-checkout-session svc …)` and does not learn that the answer now comes over a socket (BOU-90). Modelled on libs/external's outbound adapters: clj-http, `:throw-exceptions false`, errors as data rather than exceptions. FC/IS: shell. The wire contract is pure and lives in `wagoe.platform.core.rpc`.
Expose a module's protocol over HTTP, so another process can call it.
The counterpart to …rpc.client: that turns protocol calls into requests,
this turns requests back into protocol calls against the local
implementation. Together they are what lets a module run as its own service
without its callers changing (BOU-90).
FC/IS: shell. The envelope contract is pure and lives in
wagoe.platform.core.rpc.
Expose a module's protocol over HTTP, so another process can call it. The counterpart to `…rpc.client`: that turns protocol calls into requests, this turns requests back into protocol calls against the local implementation. Together they are what lets a module run as its own service without its callers changing (BOU-90). FC/IS: shell. The envelope contract is pure and lives in `wagoe.platform.core.rpc`.
Service-level interceptors for business logic operations.
This namespace provides interceptor pipelines specifically designed for service layer operations, eliminating manual observability calls and providing consistent error handling, logging, metrics, and error reporting across all service methods.
Key Benefits:
Usage: (execute-service-operation :register-user {:user-data user-data :tenant-id tenant-id} service-impl-fn {:system system})
Architecture:
Service-level interceptors for business logic operations.
This namespace provides interceptor pipelines specifically designed for service layer operations,
eliminating manual observability calls and providing consistent error handling, logging, metrics,
and error reporting across all service methods.
Key Benefits:
- Eliminates 50+ manual observability calls from service methods
- Provides consistent error handling and reporting patterns
- Automatic timing, logging, and metrics collection
- Single point of control for service-level cross-cutting concerns
Usage:
(execute-service-operation
:register-user
{:user-data user-data :tenant-id tenant-id}
service-impl-fn
{:system system})
Architecture:
- Service interceptors wrap pure business logic functions
- Context carries operation metadata and observability services
- Interceptors handle all cross-cutting concerns automaticallyauthz-report: the route table as :wagoe/authorization :enforce would
judge it, printed without serving. Builds :wagoe/http-handler and what it
depends on, never :wagoe/http-server, so it binds no port; the database
and the modules still start, as the routes need them, and so do the
migrations a config runs at start.
`authz-report`: the route table as `:wagoe/authorization :enforce` would judge it, printed without serving. Builds `:wagoe/http-handler` and what it depends on, never `:wagoe/http-server`, so it binds no port; the database and the modules still start, as the routes need them, and so do the migrations a config runs at start.
Assemble an Integrant system from a loaded configuration map.
This is the half of wagoe.system-config that is the same in every
application: the components every Wagoe app has (database, HTTP, router,
observability, i18n), the framework modules its config switches on, and the
modules bb scaffold generate produced. What is left for the application is
what only it knows — see the generated config.clj.
Before BOU-326 this lived in the generated file, 404 lines of it, and a
second copy lived in this repository's own src/wagoe/system_config.clj.
Neither copy wired :wagoe/workflow-db-schema, so wagoe add workflow
produced a module whose tables were never created.
Usage: (system-config (config/load-config) {:base-ns "shop"})
Assemble an Integrant system from a loaded configuration map.
This is the half of `wagoe.system-config` that is the same in every
application: the components every Wagoe app has (database, HTTP, router,
observability, i18n), the framework modules its config switches on, and the
modules `bb scaffold generate` produced. What is left for the application is
what only it knows — see the generated `config.clj`.
Before BOU-326 this lived in the generated file, 404 lines of it, and a
second copy lived in this repository's own `src/wagoe/system_config.clj`.
Neither copy wired `:wagoe/workflow-db-schema`, so `wagoe add workflow`
produced a module whose tables were never created.
Usage:
(system-config (config/load-config) {:base-ns "shop"})Integrant system lifecycle management for Wagoe application.
This namespace defines init-key and halt-key! multimethods for all system components, providing proper lifecycle management and dependency injection for the entire application.
Components:
Usage: (require '[wagoe.config :as config]) (require '[wagoe.system-config :as sys-config]) ; your application's (require '[integrant.core :as ig]) (def cfg (sys-config/ig-config (config/load-config))) (def system (ig/init cfg)) (ig/halt! system)
Integrant system lifecycle management for Wagoe application. This namespace defines init-key and halt-key! multimethods for all system components, providing proper lifecycle management and dependency injection for the entire application. Components: - :wagoe/db-context - Database connection pool and adapter - :wagoe/user-repository - User data persistence - :wagoe/session-repository - Session data persistence - :wagoe/user-service - User business logic orchestration - :wagoe/logging - Structured logging and audit trails - :wagoe/metrics - Application and business metrics collection - :wagoe/error-reporting - Error tracking and alerting - :wagoe/http-handler - HTTP request routing and handling - :wagoe/http-server - Jetty HTTP server Usage: (require '[wagoe.config :as config]) (require '[wagoe.system-config :as sys-config]) ; your application's (require '[integrant.core :as ig]) (def cfg (sys-config/ig-config (config/load-config))) (def system (ig/init cfg)) (ig/halt! system)
Error handling utilities for enhanced context preservation and structured error responses.
This namespace provides middleware and utilities for capturing rich error context across HTTP and CLI interfaces, enabling better debugging and observability.
Error handling utilities for enhanced context preservation and structured error responses. This namespace provides middleware and utilities for capturing rich error context across HTTP and CLI interfaces, enabling better debugging and observability.
No vars found in this namespace.
No vars found in this namespace.
No vars found in this namespace.
Port management utilities for development environment.
Provides port conflict resolution, environment detection, and intelligent port allocation for different development scenarios.
Port management utilities for development environment. Provides port conflict resolution, environment detection, and intelligent port allocation for different development scenarios.
The clock, read once per library rather than once per namespace.
current-timestamp was defined identically in utils.error-handling and
interfaces.http.middleware (BOU-302).
A shell namespace, because reading a clock is the shell's job: core functions take the instant as an argument.
The clock, read once per library rather than once per namespace. `current-timestamp` was defined identically in `utils.error-handling` and `interfaces.http.middleware` (BOU-302). A shell namespace, because reading a clock is the shell's job: core functions take the instant as an argument.
Stateful in-process registry of validation rules.
Holds mutable process state (the rule registry and execution-tracking atoms), so it lives in the shell — the functional core (wagoe.core.validation.registry) keeps only the pure rule-shape and conflict helpers. Provides registration, lookup, and coverage tracking for validation rules across modules.
Rule format: see wagoe.core.validation.registry.
Stateful in-process registry of validation rules. Holds mutable process state (the rule registry and execution-tracking atoms), so it lives in the shell — the functional core (wagoe.core.validation.registry) keeps only the pure rule-shape and conflict helpers. Provides registration, lookup, and coverage tracking for validation rules across modules. Rule format: see wagoe.core.validation.registry.
Shared helpers for table sorting, pagination, and basic search filters across web UIs.
This namespace centralizes parsing of table-related query parameters so that all modules use the same semantics for sorting and paging. It also provides small helpers for generic search/filter parameters that feature modules can interpret according to their own needs.
TableQuery shape: {:sort :name ; keyword column identifier (or default) :dir :asc ; :asc or :desc :page 1 ; 1-based page index :page-size 20 ; items per page :offset 0 ; derived from page/page-size :limit 20} ; derived from page-size
Shared helpers for table sorting, pagination, and basic search filters across web UIs.
This namespace centralizes parsing of table-related query parameters so that
all modules use the same semantics for sorting and paging. It also provides
small helpers for generic search/filter parameters that feature modules can
interpret according to their own needs.
TableQuery shape:
{:sort :name ; keyword column identifier (or default)
:dir :asc ; :asc or :desc
:page 1 ; 1-based page index
:page-size 20 ; items per page
:offset 0 ; derived from page/page-size
:limit 20} ; derived from page-size
Reading the running system — platform's introspection API.
The sanctioned surface for a tool that needs to know what is running.
Development tooling used to answer that from integrant.repl.state/system,
which only a REPL (go) fills, so a server started by wagoe.main was
invisible to it: the dev dashboard reported three components for a system of
forty-three (BOU-400). The system has always been recorded — in
wagoe.platform.shell.system.wiring, which is platform's shell, and naming
it from another module is the violation bb check:ports exists to catch.
Same shape as wagoe.platform.database, and for the same reason: a narrow
non-shell namespace over machinery that stays internal (BOU-303).
Read-only on purpose. Starting and stopping a system is a composition root's
job — wagoe.main and the REPL both call wiring/start! directly, and they
are the two places allowed to.
Reading the running system — platform's introspection API. The sanctioned surface for a tool that needs to know what is running. Development tooling used to answer that from `integrant.repl.state/system`, which only a REPL `(go)` fills, so a server started by `wagoe.main` was invisible to it: the dev dashboard reported three components for a system of forty-three (BOU-400). The system has always been recorded — in `wagoe.platform.shell.system.wiring`, which is platform's shell, and naming it from another module is the violation `bb check:ports` exists to catch. Same shape as `wagoe.platform.database`, and for the same reason: a narrow non-shell namespace over machinery that stays internal (BOU-303). Read-only on purpose. Starting and stopping a system is a composition root's job — `wagoe.main` and the REPL both call `wiring/start!` directly, and they are the two places allowed to.
Logging a repeating event once per interval, by key. Outside shell/ so
other modules can call it, as wagoe.platform.database.
Logging a repeating event once per interval, by key. Outside `shell/` so other modules can call it, as `wagoe.platform.database`.
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 |