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 (superseded by 0201)
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 — superseded by 0132; broker TLS via SSL bundles still stands
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 (amended by 0172: scopes add resource patterns and teams)
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 (amended by 0178: every node is compared, not a pair)
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)
0076The per-node rate-limit permit is taken inside JolokiaBrokerClient before every request, keyed by the Jolokia URL; a batch costs one permit per 50 operations
0077Capture acknowledges only what it stored: each drain writes its own batch, acknowledges after commit, and backs off with jitter inside its bound on a failed write
0078The pending audit row commits in its own transaction before the broker call and the outcome in a second one after it; a rolled-back caller marks the row failed
0079Capture broker objects are scoped per Studio instance, the capture queue is bounded in bytes, and the capture prefix is reserved
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 — superseded by 0161
0117The SDK's DiagramView: a read-only, keyboard-operable diagram on xyflow and the ELK worker, fed plain nodes and edges
0118Pause is enforced by TanStack's focusManager for every query, plugins included; poll() and the refresh control are removed (amends 0052)
0119Topology rediscovery is a scrape tier on scrape.discovery-interval (1m); the on-demand endpoint and its buttons are removed (implements 0004)
0120DiagramView offers caller-defined inserts on arrows (insertable, insertChoices, onInsert), box actions (nodeActions, onNodeAction; "⋯", right-click, Shift+F10), zoom controls and a CSS height; it still changes nothing itself (amends 0117)
0121The English site home is one hand-written file in site/src/public/ (inline CSS/JS, GSAP from cdnjs), an interactive story of the one-broker-per-tab hunt; the logo links to it with a full load (amends 0061)
0122Superseded by 0168. Every response carries a Content-Security-Policy (scripts, connections and workers from self, frame-ancestors 'none'); every plugin SVG is sandboxed
0123Sessions really live in JDBC (spring-boot-starter-session-jdbc); disabling a user, removing a grant or changing a role ends the affected users' sessions (amends 0037, 0103)
0124OSV-Scanner (every manifest; PRs fail on new, main and weekly on any; amended by 0171: only fixable ones fail) and CodeQL (code-scanning default setup) run on every PR; findings are fixed, false positives dismissed with a reason
0125Every ScheduledJob declares INSTANCE or INSTALLATION; installation-wide jobs run on one instance per tick through ShedLock (keep-alive, database time); plugin contract 4
0126Pull requests carry the verification (parallel, path-filtered jobs behind one required ci-ok check on an up-to-date branch); main does not re-test, and releases publish to Central and npm only when their inputs changed since the newest version there (supersedes parts of 0088, 0124)
0127A cluster-scope gauge is the sum of every queue's last sample at or before each bucket's end, within the queue's own sampling interval, with no bucket peak (supersedes 0110 D4)
0128Backend (JaCoCo, merged across CI shards) and UI (Vitest V8) coverage is measured on every PR that runs their tests and reported in the job summary and as HTML artifacts; no threshold gates a build yet
0129A release tags the merge commit and pushes only the tag; its notes are the GitHub release body, the site fetches them for releases after 2026.09.60, and the release deploy key is removed (supersedes 0051's per-release file, 0126 D5)
0130One permission catalogue (FeatureRegistry.catalogue(), built-ins then active plugins) for every reader; a permission declares globalOnly; a runtime permissions health check compares guards and plugin manifests with the catalogue and reports, never blocks (amended by 0172: the scope is enforced, globalOnly replaced by scope)
0131Everything under web/ is formatted by Prettier 3 (120 columns, single quotes, trailing commas), checked in the frontend CI job and applied by just fmt; the one-time reformat is in .git-blame-ignore-revs
0132Every stored secret is sealed under its own data key, wrapped by a versioned key-encryption key from one provider (env, file, Vault, Kubernetes); the current version is stored and changed by an online, resumable, audited rotation (supersedes 0009's key handling)
0133One SecretRedactor masks credential-like values in logs, audit parameters, error details and the broker.xml export; a leak test with planted secrets is the proof (extends 0092)
0134Every store that grows with use is a ManagedStore (@PluginApi HousekeepingContributor) with bounded retention settings, purged in batches by one installation-wide housekeeping job, one PURGE_STORE audit event per store; a build test fails for a table with no store
0135An alert rule without a cluster is installation-scoped, evaluated by InstallationSignalSources; STORAGE_QUOTA and STORAGE_HEALTH are seeded once per installation
0136API tokens rotate with a 24-hour overlap (previous secret in three columns, a late use audited), live under a live lifetime cap (90 days by default; expires_at required), are rate and concurrency limited per token and per user (fixed windows, RateLimit-*, 429), are counted per hour in api_token_usage, and are listed and revoked by token:admin holders (amends 0039)
0137McpGate wraps the SDK handler at the stateless transport: per-caller instructions and tool list, a token's tool allow-list answered as unknown tools, an installation-wide read-only mode, and an MCP_TOOL_CALL audit row parenting every call; no permission check is copied (amends 0046, 0054)
0138SettingDef.Kind.BOOLEAN, rendered as a switch
0139Every release artifact carries keyless GitHub attestations (SLSA provenance, CycloneDX SBOMs from Syft), image ones pushed to Docker Hub as referrers; pull requests gate the image on fixable high or critical Grype findings (amends 0042, 0102)
0140CI analyses every PR and main push with SonarQube Cloud (Maven scanner, JaCoCo + Vitest lcov coverage); a failing quality gate fails ci-ok; each change resolves its issues (fix, or false positive with a reason, or a scoped exclusion recorded in the ADR)
0141Plugin jars are standard signed JARs verified offline (one signer, every entry covered) against administrator-pinned publisher keys identified by public-key fingerprint; the trust decision is computed at read time, activation enforces it with acknowledgement, an off-by-default audited allowance admits unverified plugins, removing a key flags its plugins and degrades health (amends 0099, 0103)
0142Artemis 2.33.0 to 2.57.0 is supported (BrokerVersion): registration refuses an older seed, CI tests both ends, the JSON createDivert falls back to its positional arm before 2.38, and an operation that cannot fall back is a per-node VersionGate
0143Local accounts enrol TOTP (RFC 6238 on the JDK) and WebAuthn passkeys (Spring's WebAuthnRelyingPartyOperations) with recovery codes; roles carry requires_mfa (ADMIN by default) for local sessions only; admin-set trusted devices; tokens record MFA at mint; break-glass recovery at startup
0144Password policy (length, 72-byte cap, not the username, offline common-password list, optional HIBP k-anonymity); trusted proxies only; per-account DB lockout in its own transaction plus per-source and per-IP throttles; locked answers like wrong
0145Idle (user activity via X-Studio-Activity) and absolute session lifetimes as runtime settings; users and admins list and end sessions by hashed handle
0146Support bundle built in-process: redacted in-memory log buffer, a previewed snapshot the zip download is written from, diagnostics:bundle, audited
0147OTLP export of metrics, traces and logs through Boot's OpenTelemetry starter, off by default behind one switch; Micrometer Observation is the only instrumentation API (HTTP, Jolokia, Core, jobs, JDBC); SecretRedactor runs again at export on spans and OTel logs; nodes are labelled host:port, never by URL
0148Text carried in a bytes message decoded once where Studio reads it (strict UTF-8, gzip/deflate up to 2 MiB); binary is a property of the body, not the message type
0149The public API is one contract: a break passes CI only with a ! / BREAKING CHANGE: commit (oasdiff against main, which every release publishes), one {data, page, pageSize, count, hasNext} list shape, problem+json for every error, Spring API versioning with built-in deprecation headers, stated limits
0150Idempotency-Key on every mutating /api/v1 request: persisted, scoped to the user, fingerprinted, replayed for 24 hours (a ManagedStore)
0151Every release attaches the attested OpenAPI document and publishes generated @artemis-studio/client (npm) and artemis-studio-client (Central) (revisits 0019 in part)
0152Replicas coordinate through Postgres: a replica registry with heartbeats, a LISTEN/NOTIFY bus for stream frames and cache signals, per-cluster ownership leases placed by rendezvous hashing, a drain phase and /livez /readyz probes
0153A plugin declares that it needs a license, Studio stores the file as opaque bytes and shows the verdict the plugin reports, and the plugin is told when the file changes (PluginLicense, PluginLicenseChanged, StudioInfo.brokerInstances())
0154Plugins read metric history through the @PluginApi MetricHistory as a named acting user, whose account and grants are re-read on every read, through the metrics read API's own checks and clamps
0155CI's Sonar analysis skips pull requests opened by Dependabot, whose runs get no SONAR_TOKEN; the analysis of the main push after the merge covers them (amends 0140)
0156Plugins sign users in through declared PluginCredentialProviders that answer with an identity, never a principal, on the one login path; verified plugins only, confirmed on install; Studio revalidates subjects every 5 minutes; second factors cover every password account (amends 0143)
0157One token scale: primitives in theme.ts with a cssVariablesResolver passed to MantineProvider, semantic --as-* in theme.css, components read only variables (no colour-name props, raw colours or px); reduced motion zeroes the duration tokens; autoContrast for filled controls
0158Atkinson Hyperlegible Next (UI, identifiers, tabular figures) and Mono (code, bodies) as pinned @fontsource-variable packages bundled by Vite and served by Studio, never a CDN; Latin preloaded with metric-matched fallbacks; graphite, cobalt, amber and signal palette; health is quiet and stated in words
0159The colour scheme follows the system, with system, light and dark chosen from one header control; a static same-origin boot-prefs.js applies scheme and density before first paint under the CSP (amends 0005)
0160One DataTable over one column model: a virtualised CSS-grid renderer (keeps 0020, 0107, 0108) and a native static table for small read-only sets that switches to the grid above 200 rows; DescriptionList for term and value pairs; replaces VirtualTable and the raw tables (refines 0020, 0056)
0161Columns are measured in a max-content Measurer with per-kind fonts and ch bounds, then placed by a pure solver: fit, shrink, hide by priority inline-end first with hysteresis, then overflow with sticky edges; viewer widths are hard; identity and node columns never hide; state {v, widths, hidden, shown, order} (supersedes 0116, keeping its resize and keyboard interactions)
0162Compact (default) and comfortable table density, a per-browser viewer preference applied to every table before first paint, never in the URL; compact targets stay at least 24 px
0163Pages are built from shared parts (Page, PageHeader with the single h1, Section, Toolbar, the empty, error and loading states, StatusBadge, Stat, DescriptionList, ConfirmDialog, notify), all exported by the SDK; VirtualTable removed and the extension contract bumped so old plugins are refused rather than crash; exports frozen per release
0164Laid out for desktop windows from 1280 px at 100% zoom with no phone or tablet layouts; intrinsic layouts hold at 200% zoom, the nav collapses to its rail below 64rem; grids, canvases and the editor are the 2-D reflow exceptions
0165Layout and accessibility are verified in Chromium: a Vitest browser-mode project with a tables test over every grid's real columns, and a Playwright sweep of every route on an isolated QA stack checking axe, overflow, CLS, CSP and foreign requests (amends 0024)
0166Trusted publisher keys can be pinned by configuration (artemis-studio.plugins.trusted-keys, a source of ADMIN or CONFIGURATION on each key): the table is reconciled before plugins start at boot, a key dropped from configuration is un-trusted, a bad entry fails startup, a configured key cannot be removed through the API or UI, and every change is audited as configuration (amends 0141 D3, D7)
0167A broker is registered in one cluster, identified by its NodeID (the URL in normal form when it reports none): the check and the registration refuse an overlap, partial or full, with a 409 cluster-already-registered naming the cluster when the caller may see it, and a broker_identity table keyed by identity makes the second of two racing registrations find the first; claims follow the nodes (discovery, NodeID changes, URL overrides) and a claim no node backs is released, and a cloned broker sharing a NodeID is refused with no override
0168XSS is defended at output and in the browser, never by rewriting input: the CSP adds script-src-attr 'none' and Trusted Types (policies default for markup-free text, dompurify, studio#worker), every response carries Referrer-Policy, Permissions-Policy and Cross-Origin policies, downloads are attachments, lint bans HTML sinks, and links from data go through safeHref. Supersedes 0122
0169Pull requests merge through GitHub's merge queue instead of a strict up-to-date rule — superseded by 0170; merge queues need an organisation-owned repository
0170The strict up-to-date rule stays, and a pr-auto-update workflow merges main into the open PRs with auto-merge on one at a time, oldest auto-merge first, as a serial queue, with a fine-grained token so CI re-runs and auto-merge merges the tested tree (supersedes 0169)
0171OSV findings are all reported to code scanning, but only one with a fixed version fails CI (main, weekly, and what a PR introduces); a finding with no fix is a ::warning:: (amends 0124)
0172Teams own queue and address name patterns per cluster (no overlap; members, team roles, shares); every permission declares global, cluster or resource scope and what it requires; every check names its resource (404 when unreadable, 403 when forbidden), lists and streams are filtered, access is read per request (amends 0038 and 0130)
0175A cluster stores a management URL pattern (scheme://{host}:port/path, by default its first seed); a discovered node's management URL is the pattern with its connector host, attached only when the broker answering reports its NodeID, with a recorded source (seed, derived, manual) and a classified reason when there is none; a seed host with several addresses fans out (a TLS seed keeps its host name); the management and Core accounts are tried separately and a refusal is CREDENTIALS_REJECTED naming the account (amends 0013, 0004)
0176The registration check previews what adopting the running configuration would declare (counts and node disagreements), and POST /clusters with adopt saves it as revision 1 in the registration's transaction, attributed to the operator; on when the nodes agree, off when they do not; nothing adopts on its own after registration (amends 0067 D8)
0177A recommended security setting's roles are the Core account's roles read with listUser (those the broker already names for the address, else all of them); when the broker cannot list users the broker's own roles are prefilled and the reason is stated, with the source as accountRolesSource (amends 0068)
0178Config diff compares every manageable node at once: per key a majority (more than half of the nodes that return it), its outliers, a state (SAME, DIFFERENT, MISSING_ON_SOME) and ADR-0043's class (DRIFT, EXPECTED, UNCLASSIFIED); an unreadable node is listed with its reason and left out of every majority, and with fewer than two answering there is no comparison; the view opens on drift, with the filters in the URL (amends 0043 D1 and D5)
0179Gated service methods call OperationGate.run after authorizing and never on a dry run (@Gated, checked by ArchUnit); the contract is the @PluginApi module kernel.gate; one plugin approval provider, armed from its desired install status with no cache, answers allow, hold or deny; armed and unavailable fails closed (503); Studio owns held operations; contract 12
0180A held operation's parameters are canonical JSON bound by a SHA-256 hash, its request and decision AES-GCM sealed, its states kept by triggers; a vote and a run are each one conditional update; the run replays the same service method as the live requester after integrity, version, stateKey and provider checks; BY_REQUESTER for secret results; an interrupted run is OUTCOME_UNKNOWN, never rerun
0181Deciding is session-only with fresh authentication; for every provider the approver is another person (id, external identity, email), holds the approver permission now, has an older account, and the requester made no access change to anyone since the request (access_change_log); session facts go to the provider
0182A per-recipient inbox_item table (in-app links only, size checks, dedupe, bounded unread count) and one stream per user (/api/v1/me/stream); the replica bus carries a data-free signal and clients refetch; plugins post through a scoped @PluginApi Inbox
0183alert_delivery carries alert and notice kinds; plugins list channels without their configuration and enqueue rate- and size-limited notices with in-app links made absolute from the public URL; notices never carry a way to act (amends 0036)
0184artemis-studio.gate.break-glass is accepted only from the OS environment or a JVM system property (any other source fails startup); while set, gated operations run audited GATE_BYPASSED with a banner, logs and inbox items; held requests stay held
0185Settings changes apply as one gated change set with a stateKey over current values and a preview; Settings shows a form per contribution with search, defaults, one draft and request-approval review; Administration uses grouped vertical navigation (Access, Installation, Governance, Support)
0186Every danger-tone confirmation is a button held until it fills (--as-hold-duration, 1.5 s) with the mouse, a finger or Space/Enter; a click or a press let go early does nothing; the button names the action and count; focus starts on Cancel; ConfirmDialog.typedName and ConfirmByTyping are removed.
0187The gate needs two approvers while it holds anything (ApproverPool, ApprovalProvider.enforcing()); an access change that would lower them below two is refused before commit; /gate/status reports quorate; break-glass stays the recovery; the requester's reason is removed. Contract 13.
0188NavItem/NavGroup/SectionNav are the one navigation row, heading and link list (Settings, Administration, Configuration); a section change keeps the scroll; the sidebar shows only under a cluster, other pages get a back line; pair rules re-check on every edit.
0189Mantine's dialogs, drawers, menus, popovers, tooltips, collapses and progress move on the motion tokens from theme defaults, exits faster than entrances, menus from their trigger; keyboard-opened surfaces appear at once; press feedback is a 0.97 scale and hover is pointer-only; toasts sit under the header, three at most; Flow animates at most 150 dots (amends 0080, 0095); the colour scheme is chosen in the user menu (amends 0159)
0190Audit rows carry a commit-order position; Studio pushes records to declared receivers and advances their cursor on return; finished rows are guarded; retention waits for acknowledgement up to a maximum age, then records a gap and alerts; declared plugins can hold records back, and a failing hold purges nothing
0191Audit export as CSV, JSON or NDJSON behind audit:export, with the audit view's visibility and redaction, streamed from a cursor, CSV formula cells neutralised, and every export audited
0192Every uuid key is a time-ordered UUIDv7 (bearer handles stay random); each module is re-baselined to one changeset that is edited until the stable release; SchemaLintTest checks keys, foreign-key indexes and column types on the migrated catalog; Liquibase takes a session advisory lock (amends 0008, 0072)
0193Metric samples, the message index, broker events, request-reply events and the audit trail are range-partitioned in UTC with no default partition; one maintainer keeps partitions ahead and freezes closed ones; retention detaches concurrently and drops; an audit month drops only when every row is eligible (amends 0134)
0194One job runner: a virtual thread per run, the next run after the last ends, a timeout per job, an installation-wide run stops when its lock cannot be extended, a malformed cron is reported and retried, schedules in UTC; replaces 0125's job list (amends 0048)
0195A fail-fast connection pool with statement, lock, idle-in-transaction and network timeouts set on every connection; long work raises the statement timeout for its own transaction; the bundled database ships one configuration file, and Studio depends on no server setting
0196Heavy endpoints take per-user permits counted across replicas in PostgreSQL, with time and row bounds named in the response; sign-in, upload and in-flight limits are shared counters; a node's call rate is split between ready replicas; long histories page by key
0197Each event-stream subscriber holds at most 1,000 frames and 1 MiB; queued change signals coalesce; a subscriber that overflows repeatedly or blocks a write is told to reconnect and replay; broker events are written in commit order (amends 0018)
0198A standalone Gatling load test (just load, a manual workflow) drives a replicated Studio and publishes sizing figures with a regression check against docs/sizing.md
0199Studio runs on an operator's PostgreSQL 18 in any schema as a non-superuser; a boot preflight refuses old servers, missing privileges and transaction-mode pooling; failover is ridden through; heavy reads may use a lag-checked read replica
0200Each tier-C tick reads every listQueues page of every node and then reaps, so a node is fully known one tick after registration; the sweep cursor is gone (amends 0015)
0201The product is Rahdar (rah, route + dar, keeper) and the broker SDK is OpenBroker; every identity renamed with no alias, contract 15, a fresh database, the site on rahdar.dev (supersedes 0001 and 0061 D6)
0202CodeQL is turned off; SonarQube Cloud is the one static analysis and OSV-Scanner stays for dependencies (supersedes the CodeQL part of 0124)

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