Decisions
Architecture decision records for the choices that would otherwise need re-litigating every time someone asks “why not X instead?” — what was chosen, what it was chosen over, and the trade-off that made the difference.
An ADR records the “why” behind a decision, not just the “what”: the context that forced a choice, the options considered, and the consequences accepted. Specs MUST comply with the platform constitution and MAY reference an ADR for context on a technology choice; a constitution amendment requires an ADR to document the change.
The principles behind the picks
Prefer the boring option, except where boring means unsupported. Most choices here are conventional. The exceptions are deliberate, and each has a record below.
Prefer an operator with a CRD to a Helm chart with a values file. A CRD is an API that other things can compose against; a values file is a configuration blob that only its own chart understands.
Prefer open source without a licence cliff. Several records below are about avoiding a rug-pull rather than about technical merit.
Pay for a choice once. Where a component is load-bearing it is worth being deliberate; where it is replaceable it is not worth agonising over.
When a choice needs a record
A technology choice with a rejected alternative requires an ADR before merge. If you can name what it was chosen over, write the record. If nothing credible competed, it is an installation and not a decision — say so in the pull request rather than leaving it unsaid. Version bumps, chart-value changes and single-file fixes never need one.
The records
| ADR | Title | Status | Date |
|---|---|---|---|
| 0001 | Use KCL for Crossplane Compositions | Accepted | 2024-09-29 |
| 0002 | Use EKS Pod Identity over IRSA | Accepted | 2024-04-15 |
| 0003 | Use vLLM Production Stack over KServe + llm-d for v1 LLM Platform | Accepted | 2026-04-30 |
| 0004 | Use Amazon S3 Files for LLM Model Weights Storage | Accepted | 2026-05-01 |
| 0005 | GKE Standard with self-managed Cilium (not Dataplane V2, not Autopilot) | Accepted | 2026-08-18 |
| 0006 | GKE node auto-provisioning (ComputeClass) over Karpenter on GCP | Accepted | 2026-08-18 |
| 0007 | Cloud abstraction boundaries — cloud-shaped platform APIs, neutral developer APIs | Accepted | 2026-08-18 |
| 0008 | Use Flux for GitOps reconciliation | Accepted | 2026-08-21 |
| 0009 | Use Cilium instead of the AWS VPC CNI | Accepted | 2026-08-21 |
| 0010 | Use VictoriaMetrics rather than Prometheus | Accepted | 2026-08-21 |
| 0011 | Use OpenBao rather than HashiCorp Vault | Accepted | 2026-08-21 |
| 0012 | Use Crossplane and OpenTofu, split at the Kubernetes boundary | Accepted | 2026-08-21 |
| 0013 | Use Tailscale for private access rather than a bastion | Accepted | 2026-08-21 |
| 0014 | Use OpenTofu rather than Terraform | Accepted | 2026-08-21 |
| 0015 | Use Gateway API rather than Ingress | Accepted | 2026-08-21 |
| 0016 | Use Kyverno for admission policy | Accepted | 2026-08-21 |
| 0017 | Multi-cloud DNS naming — cloud-agnostic public, cloud-pinned private | Accepted | 2026-08-23 |
| 0018 | Per-cloud OpenTofu state — GCP state in GCS, AWS state in S3 | Accepted | 2026-08-25 |
| 0019 | Cross-cloud DNS federation — GKE workloads assume an AWS role for Route53 | Accepted | 2026-08-25 |
| 0020 | Harbor on GCS — native driver with Workload Identity, not S3-compatible HMAC | Accepted | 2026-08-26 |
| 0021 | Cloud Storage FUSE for LLM model weights on GCP | Accepted | 2026-08-26 |
| 0022 | One identity provider across both clouds, hosted on AWS and named by a variable | Superseded by 0024 | 2026-08-27 |
| 0023 | Secret store keys use a name grammar both clouds accept | Accepted | 2026-08-27 |
| 0024 | The identity provider is deployable on either cloud, defaulting to AWS | Accepted | 2026-08-27 |
| 0025 | Cloud-managed secret stores as the store of record, OpenBao scoped to the PKI | Accepted | 2026-08-27 |
| 0026 | Headlamp authenticates behind an auth proxy on GKE, not against the cluster | Superseded by 0032 | 2026-08-28 |
| 0027 | AWS is the primary cloud, and cross-cloud singletons live there | Accepted | 2026-08-29 |
| 0028 | Harbor’s OIDC config is set declaratively via CONFIG_OVERWRITE_JSON, not a post-install script | Accepted | 2026-08-29 |
| 0029 | RunLore and Slack over Grafana OnCall | Accepted | 2026-08-30 |
| 0030 | Vector as the log shipper | Accepted | 2026-08-30 |
| 0031 | Per-cluster observability panes; Slack and RunLore are the pager | Accepted | 2026-08-30 |
| 0032 | Workforce Identity Federation restores per-user Kubernetes RBAC on GKE | Accepted | 2026-09-02 |
| 0033 | OpenBao is the store of record, durable as a snapshot lineage, active on the primary cloud with restore-based fallback | Accepted | 2026-09-02 |
| 0034 | Human access to OpenBao is ZITADEL OIDC, authorised by project roles, with userpass kept as break-glass | Accepted | 2026-09-05 |
| 0035 | An in-house Headlamp plugin gives the App abstraction its own view | Accepted | 2026-09-09 |
| 0036 | An app’s secrets are owned by its own ZITADEL group, and the policy for each app is generated rather than templated | Accepted | 2026-09-10 |
| 0037 | Slack notifications are rendered by Alertmanager’s own templates, not by a Block Kit bridge | Accepted | 2026-09-12 |
| 0038 | Agent instructions and skills are authored once in the open formats, with the Claude-specific paths as symlinks | Accepted | 2026-09-17 |
| 0039 | go-task is the entry point to the scripts, locally and in CI, and no script depends on it | Accepted | 2026-09-17 |
| 0040 | Vendor kubernetes-event-exporter as plain manifests instead of a Helm chart | Accepted | 2026-09-21 |
Starting a new one? Copy the template.