Skip to content

Architecture Decision Records

Significant decisions in TAPPaaS are made in writing, before the code: each one is an Architecture Decision Record (ADR) in this directory, and it stays here forever — superseded ADRs are marked, never deleted. If you want to know why the platform is the way it is, this is the trail.

The Status column tracks each decision's lifecycle — Draft → Proposed → Accepted → Superseded (ADR-013 §2); Accepted — implemented means the code is live.

The 2.0 spine — the taxonomy family

ADR Status Decides
ADR-007 — TAPPaaS Taxonomy Accepted — implemented The model everything hangs on: one Site, three classification domains (People · Apps · Environments), Health as a cross-cutting lens. Detailed per domain in sub-ADRs 007a–007e (007e partial), realization (managers/controllers) in 007f.
ADR-009 — Composition Meta-Model Proposed How a deployable unit is built (module = atomic deployable unit; <module>:<service> coordinates) — composition, as distinct from ADR-007's classification.
ADR-007a — People Accepted — implemented Who uses the platform: Organization → Group → User plus a cross-cutting Role, owned by people-manager and realized in Authentik.
ADR-007b — Apps Accepted — implemented The Module rib of the taxonomy: what a module's config declares and where it lives.
ADR-007c — Environments Accepted — implemented Where apps run: an Environment's zones, domain, update window and security posture, owned by exactly one Organization — except mgmt, the Site's control-plane Environment.
ADR-007d — Site Accepted — implemented site.json — the Site's own state: nodes, repositories, schedule, default environment.
ADR-007e — Health Accepted — partially implemented (status-badge UI is future) Health as a cross-cutting lens, and the site notification target.
ADR-007f — Realization Accepted — implemented Manager → Controller → Service: how the control plane is built, and the Stack-promotion rule.

The workload ontology — ADR-022 family

The vocabulary every other ADR now borrows: what a module is, where it sits, who manages it. GLOSSARY.md at the repository root is the consolidated SSOT for these terms.

ADR Status Decides
ADR-022 — Workload Ontology Accepted The umbrella: why the ontology exists and how the ribs fit together.
ADR-022a — Administrative Domain Accepted The boundary of one administrative authority — what external is measured against. The relationship taxonomy (D6/D7) is parked for 2.1.
ADR-022b — Location Accepted Where something physically is: Site ⊃ Building ⊃ Room ⊃ Rack. Facility removed.
ADR-022c — Node and Host Accepted Node returns to its ArchiMate meaning; adds cluster member and Host; namespaces tier.
ADR-022d — Workload Classification Accepted kind — what type of thing a module is, and why it is a dispatch key.
ADR-022e — Module Scope Accepted module.tier becomes scope: site \| environment. Scope is not stack, not multiplicity, not a layer.
ADR-022f — Kind Values and OS Accepted — folded into ADR-022d The kind leaves: vm, lxc, machine (was host), application (was app), device — plus the OS facet.
ADR-022g — Management Accepted management: managed \| unmanaged; external means only "outside the Administrative Domain".
ADR-022h — Facet Register Accepted Which attributes are facets beside the single-valued kind, and the gate a new one must pass.

Platform decisions

ADR Status Decides
ADR-001 — Trunk-mode VLAN connectivity Superseded (not adopted) VMs attach on trunk ports; zones are VLANs.
ADR-002 — Dynamic VLAN configuration Superseded Zone/VLAN wiring happens at deploy time, driven by module config.
ADR-003 — Dependency management Accepted — implemented Modules declare dependsOn; install order is derived, never hardcoded.
ADR-004 — Module catalog & config cascade Accepted — L2 live; L1/L3 superseded by ADR-007 Where module configuration comes from and how overrides cascade.
ADR-005 — Variant domain architecture Superseded → ADR-007c Per-client variants of the platform (variants → environments).
ADR-006 — Identity: users and roles Accepted — SSO live; people model → ADR-007a The identity model behind SSO (Authentik) — users, groups, roles.
ADR-008 — Switch module / network infrastructure Partially implemented (as network-manager) Physical switches and APs become managed parts of the platform.
ADR-010 — VPS satellite Accepted — implemented (Debian variant) The optional off-premises satellite: public ingress, off-site backup, admin VPN.
ADR-012 — Backup enhancement Accepted (v1.0) — v1.0 additions not built yet Where PBS lives (install-resolved placement, or an external PBS consumed by URL), what a module asks to have backed up (backup:vm / backup:filesystem), and how often (site → environment → module cascade).
ADR-014 — Zone and Environment Lifecycle Accepted — implemented Creating, binding, enabling/disabling and retiring zones and environments through managers, not hand-edited zones.json.
ADR-016 — Source NAT for subnet-filtering devices Accepted — implemented Masquerade into a zone for IoT appliances that only accept sessions from their own subnet: zone-owned snat-allowed-from gate, module-local snat.json, network-manager snat verbs.
ADR-017 — Update scheduling and mothership self-update Accepted When the sweep runs and how the mothership updates itself (systemd ExecStartPre=+, schedule from site.json).
ADR-018 — SSH Identity Resolution Under Sudo Superseded in part Why the manager estate runs as tappaas, not root; the per-call-site -i sweep is superseded by the #533 ownership guard.
ADR-019 — HA and Cross-Node VM Migration Policy Accepted — implemented (v1.1) The full migrate-vm.sh matrix — HA/non-HA, live-vs-offline by CPU compatibility (--force for downtime), strict/comment round-trip, when module.json.node is rewritten. v1.1 adds node uplink redundancy (bonded LACP uplinks, #40).
ADR-020 — Declared-Field Change Model Accepted — implemented Unifies validate · drift · modify behind one desired-state resolver, one differ and one change-class taxonomy (immutable / in-place / grow-only / migrate / …); each <provider>:<service> declares how to change — or refuse — the fields it owns, in services/<svc>/fields.json. modify --set field=value is the sanctioned path (#498/#557), with a static pre-gate for what cannot change in place and a disruption gate (--force / rebootOk) decoupled from update-tappaas --force. network-manager modify <zone> --set is the second manager (#538). ADR-019 plugs into the update-node.sh hook. How it is built: ADR-020 realization.
ADR-021 — Split-Horizon DNS and Service Reachability Accepted — implemented The internal answer for a published name is always the DMZ gateway, for every zone: DNS says "go to Caddy", Caddy + Authentik decide who gets in. Authorization moves out of the address (which caused the drift) and into identity. One resolver for all three writers (network-manager split-horizon-target, D5), ending the drift where three code paths resolved the answer from different zones (#577). Reaching Caddy is a host-scoped firewall rule — the DMZ gateway /32 on tcp/80+443 (D3) — and no zone may hold dmz in access-to (D3b, invariant I5): that grant reached the whole DMZ subnet including the firewall's own GUI and SSH (#618). A wildcard cert no longer implies a wildcard record (D4), and a name with no public DNS degrades cleanly instead of failing (R3).
ADR-023 — Reverse Proxy Access Rules Accepted — implemented What Caddy lets through once a caller reaches it. Every route — the primary and each proxyRoutes entry — has its own default proxyAllowedZones plus optional proxyAccess path exceptions, so a module can publish one webhook path without publishing the whole app (#642, #643 merged). Unlisted paths fall back to the route's own access list; none denies all. Access lists sit on every handle, never on the domain, because Caddy tries path handles first.
ADR-025 — Config migrations and the upgrade path Accepted How a release brings a site's config/ forward, and what a release may assume about where a site is upgrading from. Ordered, idempotent migrations/NNNN-<slug>.sh run by tappaas-self-prepare.sh — before the rebuild and before any module, which pre-update.sh cannot be (D2) — each with --check, a backup under config/.migrations/backup/NNNN/ and a ledger entry in config/.migrations/applied (#545 backs both up). A failed migration stops the run before anything is updated and is notified through #651. The review rule: a change that renames or re-schemas anything under config/ ships with its migration and a before→after fixture test in the fast tier. No down-migrations — rollback is the backup. Every release names the oldest upgrade source it supports (D10), which is how interim code finally gets deleted. The runner's own release carries none; ADR-017 D7's updateSchedule object is the first (D12).
ADR-024 — Site Fabric Draft — placeholder Several Administrative Domains cooperating — the backup-buddy case generalised. No content decided.
ADR-026 — Managed Machines as Modules Proposed Every machine TAPPaaS manages is a module of kind: machine — cluster nodes, the satellite, and a Debian host carrying a PBS. Adds debianhost for the OS lifecycle.
ADR-027 — Module Blueprint Proposed What every module is made of, apart from its documents: the executable artifact set with MUST/SHOULD/MAY on each, the six files of a services/<service>/ directory, all of it co-located in the module's directory, and one blueprint check for every module plus a sweep to make the estate meet it. Documents are ADR-013's, contribution files ADR-015's. 00-Template is the blueprint in files, and its stubs warn that they are stubs.
ADR-028 — Release Cadence and Patching Accepted When TAPPaaS releases and how patches reach a site. The two OS families work by opposite principles: a Debian machine is changed in place and patches itself every sweep (three apt paths — guest, managed machine, locked-down vault), while a NixOS machine is built, not upgraded — it holds no flake and no channel of its own, update-os.sh forces one revision onto it, and rebuilding against the same revision changes nothing, so NixOS patches arrive only when a human moves a lock. Decides: one estate pin (templates/flake.lock, the mothership's flake following it, keeping its own build path for upgrade safety); an unstable → staging → production train on two-week boundaries (the branches main → staging → stable), putting production at most four weeks behind, where a CVE accelerates the train rather than cherry-picking a pin onto code it was never built against; a guest proves each revision before the mothership takes it; the ref stays a Hydra-built nixos-* channel so guests substitute instead of compiling; and the #680 pin-age tripwire tightens to 45 days, which detects branch EOL for free. Opens: the control plane can still not roll itself back (D10), and the satellite's dead --os nixos remains should go (#712). Includes a primer for readers new to Nix.
ADR-029 — NFS Service Proposed (v0.9) NFS shares for consumer guests, by share name: share names are unique across the Site, each has a DNS alias nfs-<share>.mgmt.internal to its server, so a module ships one mount to every site and survives a share moving. v0.9: the declarations are Site state — config/shares.json, owned by a new storage-manager whose verbs change one share at a time (nfs share add/modify/delete/purge, nfs grant/revoke, nfs show), the start of a storage control plane that can grow beyond NFS and beyond PVE hosts; consumers depend on storage:nfs from a virtual storage module (registered like cluster; the PVE nodes deliver the service); a server module only implements a hook that builds what is declared for it (pvehost in v1). A missing share leaves a mount pending; one that appears later is migrated into with consent. Access derives from one grant (two-key check, a tcp/2049 rule to the alias, a client list of the deployed consumers that mount it, squash-to-owner); NFSv4, root_squash; TLS deferred (#771); data is only destroyed by an explicit purge.
ADR-030 — IPv6 Accepted (v1.0) IPv6 for a site, behind the site.json ipv6 switch. The WAN is declared in site.json network.wan: dynamic (DHCPv6 and a delegated prefix, the default) or static (a fixed WAN address and a routed prefix), or none. The site prefix must be /56 or larger; zones take their /64 from its first /56 by an ipv6Subnet number (0–255) that every zone gets by default and an operator removes to keep a zone IPv4-only. Devices use SLAAC; VMs get AAAA names; published names are dual-stack inside the site, and Caddy answers on the WAN's IPv6 address from the internet. Firewall rules and anti-spoofing cover both families; switching off withdraws each zone's route first. ipv6-manager converges all of it in the network update.
ADR-031 — Site Liveness from the Backup Buddy Accepted — implemented A dark site cannot report itself. Every site's backup node runs a pulse: it probes the PBS of each buddy it pulls through the buddy tunnel and mails both operators when a buddy stops answering, checks its own site (uplink, firewall, mothership, own PBS), and on the pulled side watches when the buddy last pulled. A standalone pulse covers a buddy that is not TAPPaaS.
ADR-032 — Service Exposure Accepted (v1.0) — built ADR-023 stays the model and gains three things: the identity gate authenticates machine callers (an Authentik service account and a client-credentials JWT; identityGate: false only for webhooks, with a reason); a module without network:proxy is unpublished by design, and one that runs third-party code without user data lives in the DMZ (ADR-021 R1 amended); Jellyfin is published without the gate so friends and family can use its apps (SSO for the web, Quick Connect for apps, hardened at Caddy and in Jellyfin). P0 tests done.
ADR-034 — TAPPaaS User Interface Accepted (v1.0) — built, live on hrossen One web UI for a site, console: a TypeScript backend on tappaas-cicd as tappaas with a React front end, published through Caddy and the Authentik gate as console.<domain> for operators from mgmt and the admin VPN only (from elsewhere the login, then 403; the tunnel needs the site's DNS, #856), with status.<domain> read-only for the site owner once a further route can be gated, roles from Authentik groups with destructive verbs behind a typed name. It operates only through manager verbs: every change is a job with the manager's own dry run as preview, recorded in Activity. The physical view is derived from network-manager topology, shared with G6.1, and drawn with React Flow; the service catalog (#183) is the Modules page, Backstage (#91) is not adopted; the assistant runs through LiteLLM and proposes, never applies; agents (#142) use the same API through the gate with scopes. P1 to P3 (see, operate, ask) are built.
ADR-035 — Pluggable Controllers and Managers Proposed (v0.8) A module brings its own control plane: controllers and managers live in the module in tappaas-cicd's layout and use its shared libraries, are declared in its JSON, built, linked into ~/bin and recorded in one registry (config/control-plane.json) by module-manager, and register with the manager that drives them, code and instance. A controller is <application>-controller; a taken name is refused, for managers and controllers alike, while copies of one module share one registered controller. A controller from an unverified repository plugging into a foundation manager is a warning. Ansible is an option, not the default; a controller's own declarations live in <name>.json. First UniFi (in the Community unifi-os module — not FLOSS, so not in TAPPaaS), then mikrotik-controller with Ansible. Answers #430, #731's registration; first uses #847

Governance

ADR Status Decides
ADR-011 — SBOM Governance Proposed What is running on an installation and which of it needs a fix: generated per installation, one scanner per layer (Nix closure, image, Debian host, OPNsense), coverage-checked, triage committed as VEX.
ADR-013 — Documentation Structure and Standards Accepted — implemented Where documentation lives, which artifact serves which audience, and how the site syncs from source.
ADR-033 — Signed Releases and Verified Updates Draft (v0.7) — built; keys not enrolled yet Supply-chain integrity: staging and production release tags are SSH-signed by the TAPPaaS owners with keys kept in their own vaults (one-year life), plus an offline emergency key each, on their offsite backup-key stick, that can only enrol or revoke signers; each repository records its signers in .tappaas/allowed_signers, changed only by a previously trusted signer; first trust comes from a site.json introduction; the update verifies before any checkout moves and refuses rollback; built images ship a signed SHA256SUMS and are checked by hash; nixpkgs is covered through the signed flake.lock and Hydra's keys; every repository's signing status is a health-manager check and a validate gate.
ADR-015 — Community Governance and Contribution Files Draft The community-health file set (CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, GOVERNANCE, CODEOWNERS, templates) — names, per-repo placement, and contents, plus the per-module AUTHORS.md. The companion to ADR-027, which defers every contribution file here.

Writing a new ADR? Decide in writing first, before the code; the process and standards are in ADR-013 — Documentation Structure and Standards.