Skip to content
GitHub

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.

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.json as 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.