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).
| ADR | Decision |
|---|---|
| 0001 | Name "Artemis Studio"; trademark risk accepted with a cheap rename path |
| 0002 | Jolokia-first transport, Core client second, capability-gated features |
| 0003 | Real-time updates over SSE |
| 0004 | Topology by seed node + auto-discovery |
| 0005 | React 19 + Vite + Mantine 9 |
| 0006 | Own the metrics timeseries in PostgreSQL |
| 0007 | One container image, Docker Compose first |
| 0008 | Liquibase migrations; schema physical tuning |
| 0009 | Secret vaulting with JDK AES-GCM; broker TLS via SSL bundles |
| 0010 | Jolokia over a blocking RestClient; spring-boot-starter-webflux removed |
| 0011 | Persistence via JPA (Hibernate) mapped to the Liquibase-owned schema |
| 0012 | Split-brain detection requires corroborated evidence (amends 0002) |
| 0013 | A cluster is registered from a list of seed URLs (amends 0004) |
| 0014 | Lombok for boilerplate, MapStruct for layer-to-layer mapping |
| 0015 | Tiered scrape scheduler; refresh-cycle counter scheduler-owned per cluster (retires HaRefreshTask) |
| 0016 | queue_snapshot written by JDBC batch upsert, not JPA (scoped exception to 0011) |
| 0017 | Cross-node aggregation — one logical node per NodeID, scrape the live endpoint only |
| 0018 | SSE hub is SseEmitter on Spring MVC, carrying poll-derived change signals (annotates 0003; extended by 0027) |
| 0019 | Frontend API types generated from the backend's OpenAPI document |
| 0020 | The virtualized data grid lays out on shared CSS grid tracks, not <table> |
| 0021 | |
| 0022 | Dry-run is a broker-side estimate; the bulk safety cap is server-enforced |
| 0023 | |
| 0024 | Frontend DOM test harness is Vitest + Testing Library + MSW |
| 0025 | Scrape cadence applies without a restart, via SchedulingConfigurer |
| 0026 | Core client — one subscription per live node, poll loop, Studio-driven reconnect, cached capability verdict (extended by 0031) |
| 0027 | The SSE events topic carries data, with Last-Event-ID replay and coalesced derived signals (extends 0018) |
| 0028 | broker_event — buffered batch insert, bounded queue with a visible drop counter, seq PK |
| 0029 | MessageTransport — Core for read/write fidelity, Jolokia for mutations and deep pages (supersedes 0021) |
| 0030 | Request-reply correlation is notification-anchored and browse-sampled, with a disclosed coverage ceiling |
| 0031 | Pooled Core connections via pooled-jms, superseding connect-per-call |
| 0032 | Request-reply latency via Micrometer time-windowed percentiles, no persisted history in Phase 5 |
| 0033 | Metric read model — date_bin bucketing, gauge vs. counter-rate, server-clamped step/range |
| 0034 | Two-section collapsible sidebar (cluster switcher + per-cluster view nav), replacing the horizontal view strip |
| 0035 | Alert rules are a discriminated union (metric threshold / cluster state); evaluation rides the scrape tiers, not an independent timer |
| 0036 | Notification delivery is a durable Postgres queue, batched per rule per tick, with Standard Webhooks signing |
| 0037 | Session-cookie authentication (not bearer tokens) for the browser, since EventSource cannot set headers |
| 0038 | Fully dynamic permission strings, resolved once per request via a cluster→environment→global scope walk |
| 0039 | API tokens — SHA-256 hash, prefix lookup, grants intersected with the live owner |
| 0040 | OIDC — JIT provisioning, principal swap after the exchange, claim mapping re-applied every login |
| 0041 | Audit actor carries real identity and token attribution (supersedes 0023) |
| 0042 | CalVer releases published to Docker Hub on every push to main (complements 0007) |
| 0043 | Broker configuration is compared by a classified pointer diff, not a diff library |
| 0044 | Slow-consumer detection has two authorities, and the broker wins |
| 0045 | The MCP server is a capability surface (~13 intent-shaped tools), not a REST mirror; the token budget is a build failure |
| 0046 | MCP authenticates with the ADR-0039 personal API tokens — one credential store, one authorization model |
| 0047 | Two configuration planes — studio_setting (operator, live, audited) and a Spring Cloud bootstrap plane (deploy-time, {cipher}); no config server |
| 0048 | Every settings-tunable schedule is a re-reading trigger task, not a @Scheduled annotation (extends 0025) |
| 0049 | Queue 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) |
| 0051 | The changelog is generated from commit messages, one file per release (supersedes 0042's changelog mechanics) |
| 0052 | One global freshness indicator scoped to observed queries; the stream reconnects forever with a silence watchdog (modifies 0018) |
| 0053 | Broker time is normalised onto Studio's clock; skew is measured from the Jolokia response timestamp, disclosed per flow, and alerted (extends 0030, 0035) |
| 0054 | MCP discovery is the studio_help tool, not an optional resource; one catalogue generates the schemas, help, resource and server instructions (supersedes 0050) |
| 0055 | Metric charts carry time on the x-axis, not array position; a relative window advances quantized to the bucket (extends 0033) |
| 0056 | Every view is bounded and says so; the topology canvas degrades by level of detail above a node threshold (extends 0020) |
| 0057 | Connection 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) |
| 0058 | The 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) |
| 0059 | |
| 0060 | |
| 0061 | The 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 |
| 0062 | Message 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) |
| 0063 | Full-text over the message index is Postgres tsvector with the simple configuration and websearch_to_tsquery; no second datastore (extends 0059, 0062) |
| 0064 | A 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) |
| 0065 | Measured 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) |
| 0066 | The 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) |
| 0067 | A 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) |
| 0068 | The 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) |
| 0069 | Studio 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 |
| 0070 | A versioned extension contract (backend descriptor + SPI beans, frontend defineFeature + slots), a server-published feature manifest, and a closed set of navigation groups (amends 0034) |
| 0071 | Every broker write runs through BrokerCommands: guard, audit before the call, dry run, cap, per-node fan-out, audit outcome (formalises 0022, 0049) |
| 0072 | Each 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) |
| 0073 | Identity 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) |
| 0074 | Frontend kernel / ui / feature / app boundaries are enforced with eslint-plugin-boundaries |
| 0075 | Message 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) |
| 0080 | The 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) |
| 0081 | Client 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 |
| 0082 | Configuration 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) |
| 0083 | Apply 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) |
| 0084 | Queue 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) |
| 0085 | Queue 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) |
| 0086 | A 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) |
| 0087 | The 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) |
| 0088 | CI 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) |
| 0089 | Consumer 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) |
| 0090 | The 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) |
| 0091 | Bridges 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) |
| 0092 | A 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) |
| 0093 | A 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) |
| 0094 | The 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) |
| 0095 | Flow 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) |
| 0096 | A 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 |
| 0097 | A 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) |
| 0098 | Core TLS trust is per cluster through a Studio SSLContextFactory keyed by the sslContext transport parameter, never the JVM default context |
| 0099 | Runtime 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) |
| 0100 | Plugin 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>/ |
| 0101 | Each 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) |
| 0102 | The 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) |
| 0103 | Plugin 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 |
| 0104 | A 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) |
| 0105 | Email (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 |
| 0106 | Setup 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 |
| 0107 | Row 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) |
| 0108 | VirtualTable 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) |
| 0109 | Cluster 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) |
| 0110 | splitBy=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) |
| 0111 | Plugins get beans scoped to them (PluginSecrets, PluginMessaging), and message through registrations Studio converges like capture |
| 0112 | A 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) |
| 0113 | Plugins 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 |
| 0114 | Plugin 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 |
| 0115 | SDK 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 |
| 0116 | VirtualTable columns fit their content (180–480 px) and resize by drag, double-click or Ctrl+Shift+Arrow; widths per viewer under a storageKey |
| 0117 | The SDK's DiagramView: a read-only, keyboard-operable diagram on xyflow and the ELK worker, fed plain nodes and edges |