Architecture decisions
Code comments cite decisions as ADR-NNN. This page is the index: each decision in one line, with its status and where it shows in the code. The full records (context, decision, consequences) are not published in the repository. Since 2.0 the code is Go (ADR-033); paths in the last column are Go packages under internal/ unless they say otherwise.
| ADR | Decision | Status | Where it shows |
|---|---|---|---|
| 001 | Kubernetes holds desired state; SQL holds identity and audit | superseded by 015 | — |
| 002 | One binary with roles (modular monolith) | decided | kuben serve --roles, core/config |
| 003 | Two Tokio runtimes as a bulkhead, a supervisor, panic = "unwind" |
superseded by 013 (supervisor and unwind kept) | supervise |
| 004 | Projections (small read models) instead of raw reflectors | decided | kube/projection |
| 005 | Opaque cookie sessions and opaque API tokens; no JWT in the browser | decided | httpapi/auth |
| 006 | sqlx with two backends (SQLite, PostgreSQL) and a test matrix | superseded by 025 (PostgreSQL only) | — |
| 007 | OpenAPI-first: the TypeScript client is generated, CI fails on drift | decided; since 033 the spec is the source of the Go server too | packages/api-client, httpapi/gen |
| 008 | Gateway API as the networking layer | decided | kube/controller (gateway.go) |
| 009 | BuildKit + Railpack/CNB, deploy by digest, immutable releases | superseded by 016 | — |
| 010 | CEL and ValidatingAdmissionPolicy instead of admission webhooks | decided | CRD schemas in charts/kuben/crds/, packages/api/v1alpha1 |
| 011 | React 19 + TanStack for the console | decided | apps/console |
| 012 | Licensing and a clean-room boundary to the system being replaced | decided | — |
| 013 | One runtime in phase 0; a second one only behind runtime.bulkhead |
decided; moot since 033 (Go has one scheduler; runtime.bulkhead is accepted and has no effect) |
core/config |
| 014 | One SSE stream per browser tab; WebSocket only for terminals | decided | httpapi/stream |
| 015 | Data ownership: SQL references CRDs only by uid |
superseded by 025 | invariant I-17 |
| 016 | Persistent buildkitd plus a light buildctl Job; external registry in the MVP |
superseded by 028 | — |
| 017 | The binary applies its CRDs at boot (server-side apply) | decided | kube/controller (crds.go) |
| 018 | Environments are soft-deleted with a grace period | decided | kube/controller (environment.go) |
| 019 | Explicit bootstrap order; polling fallback for webhooks | decided | deployment docs |
| 020 | Append-only audit in the core; signed anchoring later | decided | httpapi (audit.go), store |
| 021 | Six crates in phase 0, split only for a stated reason | superseded by 033 (Go) | go.work |
| 022 | Threat model; Kuben’s service account is treated like cluster-admin | decided, threat model pending | charts/kuben/templates/rbac.yaml |
| 023 | Lease-based leader election for the controllers | decided | kube/leader |
| 024 | Turborepo runs every task; Bun replaces pnpm and Node; Cargo stays the Rust build graph | decided; amended by 033 (the Go module replaces Cargo) | turbo.json, package.json, bunfig.toml |
| 025 | PostgreSQL holds product intent and durable operations; Kubernetes holds what actually runs | decided (1.2) | store, store/migrations |
| 026 | Environment, placement and target as identities; Release separate from DeploymentRun; a frozen RenderPlan per run | decided (1.2) | core/ops/run, kube/render, store |
| 027 | Execution boundary: a separate cluster agent over outbound mTLS (AgentLink), two internal execution CRDs, generation fencing | decided (1.2); ExecutionTask not used yet |
agentlink, apps/kuben-agent, packages/api |
| 028 | Build trust and isolation: one rootless BuildKit Job per attempt, capacity admission, build pools in production | decided (1.2) | build |
| 029 | Registry and artifact access independent of the Kuben API | decided | integrations/oci |
| 030 | Secret authority: managed secrets are immutable revisions, sealed in SQL | decided (1.2) | keyring, kube/materializer (secrets.go) |
| 031 | Managed bootstrap (k3s) or a cluster you bring; a Gateway capability gate; public HTTPS as the release gate | decided (1.2) | cli/setup, cli/doctor, kube/discovery |
| 032 | SQL decides; Project, Environment and App become the materializer’s output, and direct edits are drift |
decided (1.2) | kube/materializer |
| 033 | Go runtime: the hub and the agent are rewritten in Go; the API contract, the CRD manifest and the SQL schema stay frozen | decided | go.work, ARCHITECTURE.md |
ADR-023 in brief: leader election without trusting clocks
Section titled “ADR-023 in brief: leader election without trusting clocks”The chart allowed several replicas on PostgreSQL, but every replica ran the full controller set and server-side-applied the same objects with force. The decision: only the holder of the Lease kuben-controller runs the controllers; every replica still serves the API from its projections. Every Lease write is a compare-and-swap on resourceVersion; a candidate treats the Lease as expired only after seeing it unchanged for the lease duration (15 s), never by comparing renewTime with its own clock, so skew cannot produce two leaders. The leader renews every 2 s and stops its controllers if renewal fails for 10 s. On shutdown it releases the Lease. Login-throttle windows moved to the database so all replicas share one budget. Read the blog post for the full reasoning.
ADR-024 in brief: one task runner for two languages
Section titled “ADR-024 in brief: one task runner for two languages”Every JS package and every Go module is a Turborepo package, with the same task names (build, test, lint, …): the Go modules of go.work (kuben, kuben-agent, api) through Turborepo’s Go workspaces. The cross-language edges are explicit: kuben#build depends on @kuben/console#build and embeds the console into the Go binary. Up to 1.2 the Rust crates were packages of Turborepo’s native Cargo workspace; since ADR-033 the Go modules took their place, and Go schedules and caches compilation. Bun 1.4 is the package manager, JS runtime and test runner; Node is not needed. Releases never reuse a cache.
ADR-033 in brief: the Go runtime
Section titled “ADR-033 in brief: the Go runtime”Kuben 1.x was written in Rust. For 2.0 the hub and the cluster agent are rewritten in Go, the language of Kubernetes, k3s, Helm and cert-manager. The maintained Kubernetes, OCI and ACME libraries (client-go, controller-runtime, envtest, controller-gen) replace code we wrote and kept by hand; builds take seconds and cross-compile without extra tooling; and many more contributors can read and change the code. It is not a performance rewrite: speed is of the same order, and the agent uses more memory. Users should see no change, so the contracts stay frozen (along with the agent protocol, the configuration and the CLI), and the Go code is checked against them:
- The API:
packages/api-client/openapi.jsonas 1.2 shipped it. The Go server is generated from it, and an oracle test compares the Go hub with the released 1.2.0 binary request by request. - The custom resources:
charts/kuben/crds/kuben.dev_all.yaml. The binary embeds the same bytes and the Go tests pin them; from 2.1 controller-gen generates the manifest from the Go types. - The SQL schema: the migrations of 1.2, byte for byte. 2.0 adds none, so going back to 1.2 is an image change; the first new migration comes with 2.1.
The release keeps the archive names, checksums and signatures of 1.x, so install.sh and the chart work unchanged. 1.2.x receives security fixes from the release/1.2 branch for six months after 2.0.