ADR-016: One app behind one chrome, branded by what is installed
Source of truth:
docs/adr/016-one-app-product-branded.md(synced at build time — edit the original, not this copy). Status: Accepted (2026-09-28). It describes a target, and the spec issue linked from the Wayfinder: Veltro as one app map tracks the build.
Decision owner: Danny
Amends:
- ADR-014’s Decision, which kept the launcher as part of the thin shared layer. The launcher goes. Decision 5 (a single product carries its own brand) stays and is extended below.
- ADR-010 §2 (“VectorFlow retains ownership of Teams… CHAD and Warden retain their product-native teams”) and ADR-015 (“teams live inside each product and identity only knows their raw ids”). Identity owns the team directory. ADR-015’s rule that IdP groups never become teams stands.
- ADR-008, which has an
approverrole only for Warden. Every product gets one. D11 moves the active team from a view-only cookie into the signed session; D8, D9 and D11’s rule that a selection narrows and never widens stand unchanged. - ADR-002, which houses the design system in
vectorflow/packages. The packages move to this repository. - ADR-006, whose MCP server always calls itself Veltro and uses
veltro://resources. - ADR-009, whose contract registry is retired (see Conformance below).
Context
Section titled “Context”ADR-014 shipped in full: an always-on core, any subset of products selected with VELTRO_PRODUCTS, and one sign-in path per product. The products still look like three applications sharing a launcher. Each assembles its own rail, header, palette, bell and settings tree from @veltrosecurity/shell parts, and a user who crosses from a pipeline to an alert to a case changes applications twice. Identity adds a fourth surface with its own console. Settings for AI, notifications, audit, account and backup exist in two to four places, and the words behind them (Team, Environment, approver) mean something different in each product.
The release path grew the same way. Of the last 80 merged PRs here, 45 were pin bumps, release cuts, or release, CI and conformance fixes. Identity had its own tag lane. A daily bot opened a pin PR for every module release. About 240 conformance gate failures in two months stopped 22 real defects, and most of the upkeep went to re-recording baselines.
Veltrosecurity is the company. VectorFlow, CHAD and Warden are products, and each is a complete answer on its own: someone may want VectorFlow only for logging, or CHAD only because they already run the rest. Veltro is what you get when two or more are installed, and it is what a hosted offering would sell.
Decision
Section titled “Decision”Products and brand
Section titled “Products and brand”- The installed set decides the brand. With one product installed, the chrome, sign-in page, titles, MCP server name and docs entry point are that product’s (VectorFlow, CHAD or Warden), and no other product’s vocabulary appears anywhere. A VectorFlow-only install has no severities, alerts, cases or security framing, and looks like VectorFlow deployed on its own. With two or more products it is Veltro.
- In Veltro, sidebar sections are named after the products, so someone who adds Warden to CHAD still sees “CHAD”.
- A product that isn’t installed is invisible: no nav entry, Home item, search result, setting, locked state or upsell.
VELTRO_PRODUCTSstays the switch, at whole-product granularity, under that name.
One chrome
Section titled “One chrome”@veltrosecurity/shellexports one complete chrome: header, sidebar grouped by product, team switcher, Settings entry, account menu, search and a single notification bell. Every app mounts it in its root layout. Links within a product use that app’s router, and links to another product are plain navigations.tokens,uiandshellmove fromvectorflow/packagesto this repository, and all four apps consume the same published version.- The sidebar comes from a runtime nav manifest. Each product serves its own section (pages, icons, required role, settings pages). Identity merges the installed products, filters by the user’s grants and active team, and serves the result. A product’s nav change ships with that product.
- The chrome is in the first paint. VectorFlow and identity render it on the server. CHAD and Warden ship a static skeleton in
index.html: header and sidebar geometry with no brand, section names or links, because a cached manifest could show a product or page the user has since lost. The real chrome replaces it once identity answers. Cross-document View Transitions keep the header and sidebar still across products, and speculation rules prefetch (never prerender) on hover. The prototype on theprototype/one-appbranch showed that this reads as one application, and that without the skeleton the SPAs blank out for about 250 ms on every hop. - URLs keep the product paths
/vectorflow,/chad/and/warden/. Home is/. Identity’s screens become routes under/settings/, and the old/#sectionand/veltro/settings/*paths redirect.
Home, search and notifications
Section titled “Home, search and notifications”- Home replaces the launcher. It is a ranked list of what needs a person across the installed products, with filter chips per product and one action per row: approvals waiting on me, cases assigned to me, new critical and high alerts, failing pipelines, offline nodes, and setup items. A one-line strip sits above it. With a security product installed, the strip counts the loop stages that are present (events, detections, alerts, cases, actions). With VectorFlow alone it shows pipeline health.
- Header search covers pages, entities by name or id, and actions. Each product answers for its own entities, and the chrome merges the results. The bell merges each product’s notifications the same way.
Settings
Section titled “Settings”- Settings has one section for the whole install and one per installed product.
- Users & access holds every product’s grants. The permission matrices for CHAD and Warden stay in their own sections.
- Settings > AI is one form with one kill switch. Saving it writes the provider settings and key into each installed product’s own secret store through that product’s API. Identity never holds an AI key, which keeps the boundary in
packages/ai-ts/README.md. Turning AI off writes “off” to every product first and reports each product’s read-back state; the switch shows off only when every product confirms, and identity retries any product that didn’t. - Identity personal access tokens become the only user token and work against MCP and every product’s REST API. CHAD API keys retire. VectorFlow service accounts stay, as machine credentials scoped to an environment.
- Notification destinations (Slack, webhook, email, PagerDuty and so on) are defined once. Each product subscribes its own events to them.
- There is one Audit page over the merged feed, filterable by product. Each product keeps its hash-chained log as storage.
- There is one Account page, identity’s, with a section contributed by each installed product.
veltro backupis the only backup. VectorFlow’s in-app database dump goes. CHAD’s export becomes “Export / import detection content”.
Team and Environment
Section titled “Team and Environment”- Identity owns the team directory: create, rename, delete, members. Names travel in claims, and every product scopes by the same team ids. CHAD and Warden stop creating nameless mirror rows from whatever id a grant carries.
- Membership holds across every installed product. A user’s role in each product still comes from that product’s grant.
- The header team switcher defaults to all of the user’s teams, and picking one narrows every product. The selection lives in the identity session and is signed into each request, replacing the
veltro_active_teamcookie no backend reads. A selection only narrows what the grants already allow: a team the user isn’t in, or no longer is in, falls back to all their teams, and nothing about the selection widens authority. Identity stops refusing users who hold two or more team grants. - A row with no team stays readable by everyone holding the product role, whatever team the switcher has picked: picking a team narrows team-owned rows only, in every product’s lists, search and Home (#1237). Creating or changing install-wide rows, and minting global actor tokens, still needs an org-wide grant (ADR-008 D8, D9); a team-only user can’t widen alert, case or run ownership.
- Environment is a VectorFlow word, and its selector appears only on VectorFlow pages. CHAD’s team-owned rule deployment scope is renamed Deployment target. Warden’s environment string stays a label on runs.
Approvals
Section titled “Approvals”- Approvals share one experience and one set of rules, and each product keeps its own store and its own meaning of approve (VectorFlow still separates approve from execute, CHAD applies on the last approval, Warden resumes the run). Enforcement stays in each product’s API and worker, so a direct API or MCP call meets the same checks as the Approvals page.
- Four-eyes holds everywhere, as #132 set it for Warden. A requester can’t approve or deny their own request. Admin is no bypass. The only exception is one an admin grants, scoped to one product environment or deployment target by its id (never its name, which can be changed) and optionally a team, with a reason and an expiry. VectorFlow’s admin bypass and CHAD’s reasoned self-approval go. What was approved stays frozen: a change to the approved content invalidates the approval (
docs/security/threat-model.mdrates a changed approved digest as Critical). - Identity grants
approverfor every product. - Every pending approval expires, after 24 hours unless a product changes it.
- Settings > Approvals holds exceptions, the default expiry and the notification destination. Each product’s settings say what needs approval and the quorum.
- Approvals happen in the app and over MCP: one cross-product list tool, and a decide tool per product.
Release and versions
Section titled “Release and versions”- This repository cuts one
vX.Y.Ztag, continuing the suite numbering fromsuite-v0.14.2. The identity image, gateway, suite charts, CLI and SDKs carry it. Theidentity-v*lane ends. - Pins resolve when a release is cut. The daily pin bot and its PRs go. A product release or a merge to
maincuts a patch and promotes it to the homelab automatically. Minor and major releases are cut by hand. Amended 2026-09-30: nothing cuts automatically any more; every release, patch included, is a dispatch ofrelease-suite.yml, batched per sprint or lane, with security fixes dispatched right away. Gateway-only changes are ordinary patches. - One-off operations such as the fixed-realm cutover stages live in their own workflow, which is deleted when the operation finishes.
- Customers see one install version,
vX.Y.Z, in the chrome footer and Settings > About, whatever the brand; a single-product install shows it without the word Veltro. About lists component builds for support. Product update checks go. Release notes are written by hand. - Product image tags, the design system,
suite-authandaikeep their own semver as internal artifacts. The VectorFlow agent keeps its own semver, and each release states which agent versions it supports. Only theveltrochart is documented.
Conformance
Section titled “Conformance”- The gates that stopped real defects or guard the supply chain stay: manifest digests and signatures, the Helm route render, promotion checks, SPDX, Grype and its allowlist, runner policy, the product boundary, the identity image scan and budgets, the auth and AI package vectors, the suite-status contract, the capability catalog lockstep, and recovery and restore plans. The identity graph shrinks to its runtime environment allowlist and route list.
- These go: the contract registry and readiness evidence, the suite-org v2 jobs, the unpublished
product-peerandretention-lifecyclepackages, the public capability inventory, composition abuse, thevectorflow-cloudledger job once disposal finishes, and the cutover-only contracts when #219 closes. Each product keeps testing its side of every contract with its own vectors.
Docs and MCP
Section titled “Docs and MCP”- The docs site is organised by product. Each product’s section stands alone with its own install page, and a Veltro section covers what needs two or more. The CLI stays
veltro. - MCP follows the brand rule: server name, instructions, resource schemes (
vectorflow://,chad://,warden://), prompts and OAuth scopes follow the installed products. Tool prefixes stay.
Consequences
Section titled “Consequences”- Every product frontend changes shape. CHAD and Warden lose their own rail, header, palette and bell, and gain a first-paint skeleton. VectorFlow loses its dashboard chrome and its local settings for AI, backup, webhooks and suite audit.
- Identity grows: nav manifest, team directory, settings fan-out, merged search and notifications, Home, and routed settings pages. It remains the one service every installation runs.
- A chrome change reaches users only when all four apps ship a build with the new shell. The single release train absorbs that.
- Product APIs gain endpoints for nav, search, notifications, Home items and settings writes. Each is a new cross-product seam and needs a test in the product that serves it.
- Changing four-eyes and expiry changes behaviour existing users rely on: VectorFlow admins can no longer deploy past an approval gate, and CHAD authors can no longer approve their own deployments.
- Only the homelab runs these products today, so no step needs a deprecation window for outside users.
Rejected alternatives
Section titled “Rejected alternatives”- One single-page application. It would hide every reload, but it means rewriting CHAD and Warden into VectorFlow’s stack. The prototype showed a skeleton and View Transitions are enough.
- A chrome bundle loaded at runtime. It would ship chrome changes without rebuilding products, at the cost of client-only rendering, a router bridge in every app and two copies of React on each page.
- Module Federation, import maps, single-spa or iframes. None fits a Next 16 app beside two Vite SPAs; see the research on the
research/one-chromebranch. - Always branded Veltro. A VectorFlow-only user would meet a security brand for a logging tool.
- Showing products that aren’t installed as locked. Useful for a vendor selling seats, noise for someone who chose one product.
- Identity holding the AI key. It would make identity a secret distributor, reversing a documented boundary for one form’s convenience.
- A shared approval service. Three stores approve three different things with different binding and invalidation rules. Unifying the rules and the experience removes the inconsistency without migrating the stores.
- A cross-product Inventory. It suits an endpoint proxy that sees the estate. Here the only entity all three products know is a hostname, with nothing to join on.