Skip to content
GitHub

Development setup

  • Go, the version in go.work (three modules: the hub in apps/kuben, the agent in apps/kuben-agent and what both share in packages/api; the pinned tools are the module in tools/)
  • Bun 1.4 or later: package manager, JS runtime and test runner. Node is not needed.
  • Turborepo runs every task in both languages from one graph (installed by bun install).
  • kind for the end-to-end tests, Helm for the chart.
  1. Clone and set up. setup checks for Go and Bun, installs the JS dependencies and downloads the Go modules.

    Terminal window
    git clone https://github.com/Teamtem-dev/kuben.git
    cd kuben
    bun run setup
  2. Start a PostgreSQL. Kuben keeps its data only there, in dev mode too, and has no default database:

    Terminal window
    docker run -d --name kuben-postgres -e POSTGRES_PASSWORD=kuben -p 5432:5432 postgres:17-alpine
    export KUBEN_DATABASE__URL=postgres://postgres:kuben@localhost:5432/postgres
  3. Run the API and the console. The API listens on :8080 in dev mode (pretty debug logs, insecure cookie) and uses the database in KUBEN_DATABASE__URL; Vite serves the console on :5173 and proxies /api to it.

    Terminal window
    bun run dev

    The first start opens the setup page, where you create the admin account (no token is needed on localhost).

  4. Before pushing, run what CI runs: gofumpt, go vet and the static checks of scripts/go-check.sh (exhaustive switches, NilAway), go test -race, govulncheck, the TypeScript checks, Biome, the generated-file drift check and the web build with its size budget. Tests that need PostgreSQL (KUBEN_TEST_PG_URL) or a Kubernetes API server (envtest) skip locally without them; CI runs them.

    Terminal window
    bun run ci
  5. If you touched controllers or the API, also run the end-to-end suite against a kind cluster:

    Terminal window
    kind create cluster --config deploy/kind.yaml
    bun run e2e
  • Directoryapps/
    • Directoryconsole/ React 19 + TanStack, Tailwind v4 (embedded into the binary)
      • …
    • Directorysite/ this website: Astro + Starlight
      • …
    • Directorykuben/ the hub, a Go module: the server and the kuben CLI
      • Directorycmd/kuben/ its main package
        • …
      • Directoryinternal/ every hub package (ARCHITECTURE.md lists them and the dependency rules)
        • Directorycli/ the CLI: flags and output
          • …
        • Directoryserver/ kuben serve: wiring, background work, shutdown
          • …
        • Directoryhttpapi/ HTTP routes (generated from openapi.json by ogen), auth, the event stream, embedded console
          • …
        • Directorykube/ controllers, materializer, projections, leader election, rendering
          • …
        • Directoryagentlink/ the hub’s end of the hub–agent link
          • …
        • Directorystore/ PostgreSQL repositories and the migrations
          • …
        • Directorycore/ config, ids, authz, errors and the other pure domain packages
          • …
      • CONVENTIONS.md (the Go rules every package follows) and history/ (the record of the Rust → Go port)
    • Directorykuben-agent/ the agent that runs inside member clusters, a Go module
      • …
  • Directorypackages/
    • Directoryapi/ what the hub and the agent share, a Go module: the kuben.dev CRD types and the AgentLink protocol
      • …
    • Directoryapi-client/ openapi.json (the frozen API contract) and the generated TypeScript types
      • …
    • Directorytypescript-config/ the shared TypeScript settings
      • …
  • Directorytools/ pinned Go tools (gofumpt, ogen, govulncheck, NilAway, …), outside the Go workspace
    • …
  • Directorycharts/kuben/ the Helm chart and the frozen CRD manifest
    • …
  • Directorydeploy/ Dockerfiles and the kind configuration
    • …
  • Directoryscripts/ ci.sh, e2e.sh, budgets, scans, change detection
    • …
  • go.work the Go workspace
  • turbo.json the task graph
Command Does
bun run build the hub with the console embedded (apps/kuben/bin/kuben), the agent (apps/kuben-agent/bin/kuben-agent) and the web bundles
bun run check tsc, astro check and go vet
bun run lint scripts/go-check.sh in each Go module, and Biome
bun run format gofumpt and Biome fixes
bun run test go test -race and bun test
bun run gen regenerate schema.d.ts and the Go API server from the frozen openapi.json, and the CRD types’ DeepCopy
bun run drift regenerate and fail on any difference (what CI does)
bun turbo run test --filter=kuben only the hub’s Go tests (kuben-agent and api are the other Go packages)
bun turbo run size --filter=kuben the hub with the embedded console, checked against the binary budget
  • shadcn/ui components. src/components/ui/ holds the components as the shadcn CLI generated them (style new-york, on Radix and Base UI), kept byte-comparable with upstream. Changes to them are listed in src/components/ui/PATCHES.md; after shadcn add <name> --overwrite, run bun scripts/rtl-codemod.ts and re-apply those patches. Kuben’s own shared pieces are in src/components/ (kit.tsx, data-table.tsx).
  • Tokens, not colours. Colours, radius and chart colours are CSS variables in src/styles/theme.css, one block for light and one for dark, used through the semantic utilities (bg-background, text-muted-foreground, bg-primary, text-destructive, border-border, …). A raw palette colour (text-slate-400) would look wrong in one of the themes.
  • Both directions. Persian is right to left: use logical utilities (ms-, pe-, start-, border-e) and mark code, commands and addresses dir="ltr". The RTL codemod rewrites the generated components, and src/lib/rtl.test.ts fails when one of them still has physical classes. Interface text lives in src/lib/messages.ts and src/lib/messages/; the Persian table has the English table’s type, so a missing translation does not compile.
  • A strict content security policy. Kuben serves the console with script-src 'self'; style-src 'self', nothing inline. Libraries that inject <style> elements are handled at build time (vite-plugins/csp-styles.ts), and react-style-singleton is replaced by src/lib/style-singleton.ts, which uses constructed stylesheets. src/lib/csp.test.ts fails on inline scripts, style elements or injected HTML, and the browser tests check for CSP violations on every page.
  • Self-hosted fonts only. Inter’s Latin subsets ship in src/assets/fonts with their licence; nothing is loaded from another origin (font-src 'self').
  • Dependencies. Go versions live in each module’s go.mod (the pinned tools in tools/go.mod); JS versions only in the root package.json catalog (packages write catalog:). A new Go dependency needs a reason in the pull request, and govulncheck must stay clean.
  • The contracts are frozen (ADR-033). packages/api-client/openapi.json, charts/kuben/crds/kuben.dev_all.yaml and the SQL schema are the ones 1.2 shipped. The Go server is generated from the spec (bun turbo run gen --filter=kuben), and the Go tests pin the embedded CRDs to the manifest. Changing a contract means editing that file on purpose, then bun run gen for the TypeScript types; CI fails on drift, and on a breaking API change without the breaking label.
  • Every operation has a unique operationId in the spec; the TypeScript types, the Go handlers and the audit log are keyed by it.
  • CRD types stay structural: no internally tagged unions; use one-of structs with optional fields (Source { image, git }). A test guards this.
  • Budgets are gates: binary ≤ 45 MiB, image (Kuben and its agent) ≤ 50 MiB, console initial JS ≤ 200 kB, every lazily loaded chunk ≤ 80 kB and all CSS ≤ 25 kB (brotli). Raising a budget needs a written reason in the pull request.
  • Commits follow Conventional Commits (feat(api): …, fix(controller): …). Label pull requests (feature, bug, security, breaking); release notes are grouped by label.
  • Migrations are PostgreSQL only (ADR-025), in apps/kuben/internal/store/migrations/. 2.0 adds none, so going back to 1.2 is an image change; the next migration (0035) comes with 2.1.

CONTRIBUTING.md lists 18 invariants, each closing a class of bug found in the system Kuben replaces: opaque sessions and no JWT in the browser, Argon2id only, no injection through user strings, no global mutable cluster context, informers instead of polling, readiness only after sync, embedded migrations under a lock, one source of truth per datum, server-side apply with conditions, and more. Reviewers check the ones a change touches. Read it before opening a pull request.