Skip to content

Architecture Decision Records ​

Binding. Design, code, dependencies and APIs comply with every accepted ADR here. Before proposing or implementing, skim this directory for decisions touching your scope. A new significant decision — adding a technology, changing a pattern, departing from a convention — needs a new sequential ADR before or with the change. Never edit an accepted ADR's decision; supersede it.

Format: NNNN-kebab-title.md, English, Nygard style (000-template.md).

ADRDecision
0001Name "Artemis Studio"; trademark risk accepted with a cheap rename path
0002Jolokia-first transport, Core client second, capability-gated features
0003Real-time updates over SSE
0004Topology by seed node + auto-discovery
0005React 19 + Vite + Mantine 9
0006Own the metrics timeseries in PostgreSQL
0007One container image, Docker Compose first
0008Liquibase migrations; schema physical tuning
0009Secret vaulting with JDK AES-GCM; broker TLS via SSL bundles
0010Jolokia over a blocking RestClient; spring-boot-starter-webflux removed
0011Persistence via JPA (Hibernate) mapped to the Liquibase-owned schema
0012Split-brain detection requires corroborated evidence (amends 0002)
0013A cluster is registered from a list of seed URLs (amends 0004)
0014Lombok for boilerplate, MapStruct for layer-to-layer mapping
0015Tiered scrape scheduler; refresh-cycle counter scheduler-owned per cluster (retires HaRefreshTask)
0016queue_snapshot written by JDBC batch upsert, not JPA (scoped exception to 0011)
0017Cross-node aggregation — one logical node per NodeID, scrape the live endpoint only
0018SSE hub is SseEmitter on Spring MVC, carrying poll-derived change signals (annotates 0003; extended by 0027)
0019Frontend API types generated from the backend's OpenAPI document
0020The virtualized data grid lays out on shared CSS grid tracks, not <table>
0021Phase 3 message operations are Jolokia-only — superseded by 0029
0022Dry-run is a broker-side estimate; the bulk safety cap is server-enforced
0023Audit actor resolution before authentication exists — superseded by 0041
0024Frontend DOM test harness is Vitest + Testing Library + MSW
0025Scrape cadence applies without a restart, via SchedulingConfigurer
0026Core client — one subscription per live node, poll loop, Studio-driven reconnect, cached capability verdict (extended by 0031)
0027The SSE events topic carries data, with Last-Event-ID replay and coalesced derived signals (extends 0018)
0028broker_event — buffered batch insert, bounded queue with a visible drop counter, seq PK
0029MessageTransport — Core for read/write fidelity, Jolokia for mutations and deep pages (supersedes 0021)
0030Request-reply correlation is notification-anchored and browse-sampled, with a disclosed coverage ceiling
0031Pooled Core connections via pooled-jms, superseding connect-per-call
0032Request-reply latency via Micrometer time-windowed percentiles, no persisted history in Phase 5
0033Metric read model — date_bin bucketing, gauge vs. counter-rate, server-clamped step/range
0034Two-section collapsible sidebar (cluster switcher + per-cluster view nav), replacing the horizontal view strip
0035Alert rules are a discriminated union (metric threshold / cluster state); evaluation rides the scrape tiers, not an independent timer
0036Notification delivery is a durable Postgres queue, batched per rule per tick, with Standard Webhooks signing
0037Session-cookie authentication (not bearer tokens) for the browser, since EventSource cannot set headers
0038Fully dynamic permission strings, resolved once per request via a cluster→environment→global scope walk
0039API tokens — SHA-256 hash, prefix lookup, grants intersected with the live owner
0040OIDC — JIT provisioning, principal swap after the exchange, claim mapping re-applied every login
0041Audit actor carries real identity and token attribution (supersedes 0023)
0042CalVer releases published to Docker Hub on every push to main (complements 0007)
0043Broker configuration is compared by a classified pointer diff, not a diff library
0044Slow-consumer detection has two authorities, and the broker wins
0045The MCP server is a capability surface (~13 intent-shaped tools), not a REST mirror; the token budget is a build failure
0046MCP authenticates with the ADR-0039 personal API tokens — one credential store, one authorization model
0047Two configuration planes — studio_setting (operator, live, audited) and a Spring Cloud bootstrap plane (deploy-time, {cipher}); no config server
0048Every settings-tunable schedule is a re-reading trigger task, not a @Scheduled annotation (extends 0025)
0049Queue and address lifecycle is a cluster-wide fan-out that reports per node, never rolls back, and makes managementWrite evidence-backed
0050(superseded by 0054) The MCP listing budget scales per tool and enum/body detail moves to studio://tools (extends 0045)
0051The changelog is generated from commit messages, one file per release (supersedes 0042's changelog mechanics)
0052One global freshness indicator scoped to observed queries; the stream reconnects forever with a silence watchdog (modifies 0018)
0053Broker time is normalised onto Studio's clock; skew is measured from the Jolokia response timestamp, disclosed per flow, and alerted (extends 0030, 0035)
0054MCP discovery is the studio_help tool, not an optional resource; one catalogue generates the schemas, help, resource and server instructions (supersedes 0050)
0055Metric charts carry time on the x-axis, not array position; a relative window advances quantized to the bucket (extends 0033)
0056Every view is bounded and says so; the topology canvas degrades by level of detail above a node threshold (extends 0020)
0057Connection identifiers are node-local and ephemeral: a close by id names a node, a vanished target is a success, and the confirmation names the client (extends 0049)
0058The SQL Console is a restricted dialect parsed to an AST and validated against a whitelist; predicates split into selector pushdown and residual scan; SELECT-only (extends 0021, 0022)
0059The message index is opt-in per queue, retention-bounded, disposable, and treated as retained payload — superseded by 0062
0060A live tail is polled, never consuming or mutating, and states permanently that it is a sample — superseded by 0062
0061The documentation site is VitePress under site/, deployed to GitHub Pages by its own Actions workflow; docs/ stays the single source and the README is trilingual
0062Message capture is a non-exclusive divert into a ring-bounded Studio queue drained by a Core consumer; address-scoped, per-node, instance-owned (supersedes 0059, 0060)
0063Full-text over the message index is Postgres tsvector with the simple configuration and websearch_to_tsquery; no second datastore (extends 0059, 0062)
0064A console query is submitted by POST and streamed by reference, so query text never reaches a URL (modifies 0058, departs from 0003 for this stream)
0065Measured on Artemis 2.56.0: diverts, address settings and security settings created over management survive a broker restart, so the hazard is configuration drift rather than evaporation and a divert's origin is not derivable (corrects 0062, corrects the assumption absorbed from change 04)
0066The capture address auto-creates and a tap is healthy only while it is being drained: measured on Artemis 2.56.0, a divert replicates to a promoted backup but its non-durable queue does not, and the producer — not Studio — is what fails (refines 0062)
0067A cluster's address settings, security settings, diverts, addresses and queues are declared in Studio, applied over the management API canary-first and halting on the first failure, with hazards named before any write, replace semantics disclosed, only Studio-applied items ever removed, and drift advisory — evaluation scheduled, action never (extends 0049, 0065; records the advisory-only decision change 05 intended; absorbs change 05)
0068The capability probe's appliable gaps become a declaration the operator saves in one action — seeded from the node so replace semantics hold no surprises, recorded as RECOMMENDED, applied through the ordinary canary-first gates; the gaps Studio can never close are named with their fragment, and a capability is read back rather than asserted (extends 0067, 0049, 0065)
0069Studio is a kernel + platform + feature-plugin modular monolith, feature-first on both sides, composed at build time, gated by artemis-studio.features.<id>.enabled, and verified by Spring Modulith and ArchUnit in one Maven module
0070A versioned extension contract (backend descriptor + SPI beans, frontend defineFeature + slots), a server-published feature manifest, and a closed set of navigation groups (amends 0034)
0071Every broker write runs through BrokerCommands: guard, audit before the call, dry run, cap, per-node fan-out, audit outcome (formalises 0022, 0049)
0072Each module owns its tables; the schema history is re-baselined once per module, breaking upgrades while pre-stable; audit events drop their foreign keys (supersedes 0008's history continuity)
0073Identity is a sealed provider SPI (credential, redirect, bearer); external users are keyed by provider and subject and group mappings are per provider (supersedes 0040's mapping model)
0074Frontend kernel / ui / feature / app boundaries are enforced with eslint-plugin-boundaries
0075Message content passes a content policy at typed choke points: rules and checksum detectors mask on every egress path, credentials are dropped, stored copies are masked with sealed originals, message:clear shows the rest by grant and is audited (constrains the parked replay change)
0080The flow graph is laid out by ELK layered in a web worker with fixed columns, and shows flow as budgeted SVG animateMotion dots that pause natively and never carry meaning alone (rate encoding superseded by 0095)
0081Client activity is sampled only while a cluster's flow is observed: a lease renewed by flow reads, the cluster lock, one POST per node carrying client listings and routing reads, per-object deltas into shared disposable caches
0082Configuration apply updates a divergent queue (read-merge updateQueue) or address (updateAddress, dropping a bound routing type is High) instead of reporting a finding it cannot close; it still never destroys a queue or an address (extends 0067 D6)
0083Apply keeps a routing type that queues of that type are bound to and reports it as a DIVERGENT_ADDRESS finding, because the broker refuses the removal (amends 0082 D2)
0084Queue delete checks each node first: it removes the diverts that forward into the queue's address when it is the address's last queue (named in the preview, deleted before the queue, with a warning that a declared one comes back), keeps diverts whose source is that address and capture taps, and disconnects consumers only when disconnectConsumers is set (extends 0049, 0071)
0085Queue delete removes a divert into the queue's address only when the delete leaves that address with no queue and no divert bound; a divert from the address keeps it bound, so the incoming divert is kept and named (amends 0084 D1)
0086A SQL query with no source qualifier reads the index only where an enabled CAPTURE subscription covers every target; a queue that is only sampled is read from the live brokers, and FROM index."Q" still reads a sampled index (extends 0058 D3)
0087The Configuration screen is one desired-vs-live view with a per-row live state in words and a status bar; plan, hazards, confirmation and result move into a drawer on it, the separate apply route and the drift tab are deleted, and a row's "Apply this" narrows the plan by step identifier with the hash recomputed over the narrowed plan (extends 0067 D3/D7/D12)
0088CI jobs run only for the paths a change touches; a push to main releases only when it changes what the image is built from (supersedes 0042's every-push trigger)
0089Consumer health is one fixed, ordered verdict ladder computed once in feature/triage and shared by the screen, the REST API, the MCP tool and an alert condition; deliveringCount separates a stalled consumer from a starved one, the broker's own verdict still wins, and an uncomputable verdict is never reported as healthy (extends 0044; makes AlertCondition a real extension point)
0090The routing builder edits the declaration and is a mode of the one configuration screen: composing a route writes a document, the apply drawer stays the preview, the graph is keyboard-operable, bounded, motionless and never the only path, and element state is declared-versus-observed rather than origin (extends 0067, 0087; applies 0065 D2, 0080)
0091Bridges become declared items applied by the configuration engine, superseding the prohibition on mutating them: measured on 2.44.0 — a hyphenated createBridge(String) document, a nested transformer-configuration, unknown keys accepted and ignored, thirteen of twenty-three fields read back, concurrency suffixing the object name, and everything surviving a restart; a change is a remove-and-create, verification claims only what the broker reports, and a transformer is ordinary drift (supersedes routing-management's bridge prohibition; extends 0049, 0065, 0067, 0071)
0092A bridge's credential lives in SecretVault and the declaration carries only a reference; six disclosure paths — document, revision, diff, audit parameters, MCP responses, export — are closed explicitly with a test each, export names the credential it omits, and a wrong password surfaces as started-but-not-connected rather than as drift (extends 0009, 0075; depends on 0091 D5)
0093A bulk run is one operation over a preview-frozen, hashed queue set, executed sequentially through the unchanged single-queue commands on a persisted, stoppable run; restart interrupts and never resumes; queue-count cap without override plus the message cap; audit children linked by parent_id bound through a ScopedValue (builds on 0022, 0069, 0070, 0078)
0094The routing builder is the Routing screen's Builder tab, contributed by brokerconfig through a new routing.tabs kernel slot, with its own apply drawer; Configuration's routing tab is removed without a redirect; a divert's target queue is declared inline from the divert editor as its own revision, never created on a broker (supersedes 0090 D3's host screen; keeps 0090 D1/D2)
0095Flow edges take width (2–10px), dot speed (6–1.8 s) and dot count from one square-root value of the rate capped at 1000 msg/s; weight is monochrome, the line state sets only the dash, and the legend draws real reference lines (supersedes 0080's encoding clause)
0096A primitive property is required in the OpenAPI contract, requests and responses alike, set once by a springdoc PropertyCustomizer, because Jackson 3 refuses a body that omits one; a field that may be absent is a nullable reference type, and each endpoint is tested over HTTP with the payloads the frontend sends
0097A cross-broker move stages the frozen selection in a Studio-owned durable queue on the source broker, relays it over the Core API committing target before source with _AMQ_DUPL_ID, and is resumable or returnable; copy browses with a Postgres id ledger; targets that would silently drop are refused (builds on 0022, 0078, 0093)
0098Core TLS trust is per cluster through a Studio SSLContextFactory keyed by the sslContext transport parameter, never the JVM default context
0099Runtime plugins are uploaded in the UI into Postgres and run as Spring child contexts (own classloader, curated @PluginApi parent, host-imported infrastructure) behind one HTTP gateway and dynamic registries; activation is Instant, Brief maintenance or Restart; a plugin never stops Studio (safe mode) (supersedes in part 0069)
0100Plugin UIs are Module Federation remotes that take React, Mantine, TanStack and the host's SDK as import:false singletons; bootstrap loads remotes before creating the router; plugin routes live under /p/<id>/
0101Each plugin owns schema plugin_<id>, a 3-connection pool and its own EntityManager factory; migrations via Liquibase CommandScope under an advisory lock; no objects or foreign keys in public (amends 0072)
0102The plain jar is the plugin API on Maven Central (runnable jar becomes -exec), @PluginApi types are japicmp-gated against Contract.VERSION, and @artemis-studio/plugin-sdk is on npm via trusted publishing (amends 0042)
0103Plugin management belongs to an installer tier kept outside roles and checked per request, and every lifecycle action needs a ≤5-minute step-up (password, or OIDC prompt=login/max_age bound to the same subject); session ids rotate on sign-in
0104A plugin that needs a restart has Studio exit gracefully (code 75) for its supervisor to start it again — only when plugins.restart.supervised is set (compose) or on Kubernetes, with the installer's consent in the review, or by a guarded, audited, cooled-down "Restart Studio"; never the actuator restart endpoint (amends 0099)
0105Email (Jakarta Mail), Microsoft Teams (Adaptive Card) and PagerDuty Events v2 channels on the existing delivery path; PagerDuty deduplicates per rule and subject so a resent row is safe
0106Setup review is a pure rule catalogue over one batched read per node, persisted per cluster, with audited risk acceptance and a SETUP_RISK alert signal
0107Row actions are per-resource kernel slots (queue.actions …) with a closed set of sections, rendered in one anchored menu per grid (right-click, Actions button, Shift+F10); blocked items stay focusable with their reason; dialogs live in a host that outlives the row; link slots are built-in only; Flow menus navigate only (amends 0070)
0108VirtualTable is one tab stop with APG roving cell focus: Enter opens the row, Space selects, Shift+F10 opens its menu; active cell tracked by key and always rendered; widget cells focus their control; full values copyable by keyboard (extends 0020)
0109Cluster switch keeps the view; title and breadcrumb from a kernel store; palette gets the query and searches only snapshots while open; recents; declared g-letter, ? and / shortcuts that are ignored in editors, dialogs and menus and can be turned off (extends 0034, 0052)
0110splitBy=NODE per-queue metric series and opt-in byNode flow breakdown from the per-node rows; Flow Split layout with a metrics-contributed trend slot and small multiples; cluster gauge averaging recorded as a defect (amends 0033, extends 0095)
0111Plugins get beans scoped to them (PluginSecrets, PluginMessaging), and message through registrations Studio converges like capture
0112A registration states its concurrency (consumer 1..32, tap 1); consumers take no prefetch (consumerWindowSize=0); group order is the broker's message grouping; plugin drains run on Core factories with their own bounded thread pools (max-threads); contract version 2 (amends 0111)
0113Plugins declare metrics and implement PluginMetricSource; Studio samples them on tier B into metric_sample (PLUGIN), a Prometheus MultiGauge and threshold rules; declared default rules are seeded once per cluster; SDK MetricChart
0114Plugin MCP tools declare permission, scope and params in plugin.json; McpPluginBridge checks the permission before the tool runs (cluster denials hidden, global ones named) and refuses undeclared tools, a cluster tool without clusterId and a posture that contradicts readOnlyHint; contract 3
0115SDK CodeEditor on CodeMirror with @codemirror/lang-yaml and lang-json; diagnostics by line and column come from the caller; theme shared with the SQL editor
0116VirtualTable columns fit their content (180–480 px) and resize by drag, double-click or Ctrl+Shift+Arrow; widths per viewer under a storageKey
0117The SDK's DiagramView: a read-only, keyboard-operable diagram on xyflow and the ELK worker, fed plain nodes and edges

Apache-2.0. Apache ActiveMQ and Apache ActiveMQ Artemis are trademarks of the Apache Software Foundation. Artemis Studio is an independent project, not produced by, endorsed by, or affiliated with the ASF.