Skip to main content

Architecture

Tiaki consists of two independently deployable components that communicate over a REST API.

System overview

Browser

│ tRPC (TypeScript)

┌─────────────────────────────────────┐
│ Control Plane │
│ ┌────────────┐ ┌───────────────┐ │
│ │ React │ │ Express/tRPC │ │
│ │ Frontend │ │ API Server │ │
│ └────────────┘ └──────┬────────┘ │
│ │ │
│ ┌──────▼────────┐ │
│ │ PostgreSQL │ │
│ └───────────────┘ │
└─────────────────────┬───────────────┘
│ REST (proto/api.yaml)
┌──────────┴──────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Docker Agent │ │ K8s Agent │
│ (Go binary) │ │ (Go binary) │
└────────┬────────┘ └────────┬────────┘
│ │
Docker socket K8s API server
(local/remote) (in-cluster)

Control plane (control/)

Built with TypeScript/Node.js, the control plane has two sub-packages:

Server (control/server/)

  • Framework: Express.js with tRPC for type-safe browser ↔ server communication
  • Database: PostgreSQL via Drizzle ORM
  • Auth: Bearer token authentication for agents; JWT sessions for the web UI
  • Scheduler: Cron-based scan trigger using the SCAN_INTERVAL env var
  • Notifications: SMTP email via Nodemailer

Key files:

  • src/index.ts — Express app setup, routes, middleware
  • src/trpc.ts — tRPC router and agent auth middleware
  • src/routers/ — tRPC procedure definitions (agents, containers, updates, deployments)
  • src/db/ — Drizzle schema and migrations
  • src/scheduler/ — Cron job for triggering scans

Client (control/client/)

  • Framework: React + Vite
  • Styling: TailwindCSS + shadcn/ui
  • State: React Query (via tRPC client)

Agents (agent/)

Written in Go 1.23. Two separate binaries share most internal packages:

PackageResponsibility
cmd/dockerDocker agent entrypoint
cmd/k8sKubernetes agent entrypoint
internal/configEnv-based configuration parsing
internal/dockerDocker SDK wrapper — list containers, pull images, restart services
internal/k8sKubernetes client wrapper — list pods/deployments, update image refs
internal/registryDocker registry client — check for newer tags/digests
internal/reporterHTTP client for control plane REST API
internal/executorDeployment execution logic
internal/composeDocker Compose file parsing and updating
internal/gitGit commit integration
internal/trivyTrivy vulnerability scanner integration

Agent lifecycle

Agent start


Register with control plane
(POST /api/v1/agents/register)


┌─────────────────────────────────┐
│ Main loop │
│ │
│ 1. Scan containers │
│ 2. Check registries for updates│
│ 3. Submit report │
│ (POST /api/v1/reports/submit)│
│ │
│ 4. Long-poll for commands │
│ (GET /api/v1/reports/commands│
│ 30s timeout) │
│ │
│ 5. If command received: │
│ - Pull new image │
│ - Update compose file │
│ - Restart container │
│ - Report result │
│ - Optional: git commit │
│ │
│ 6. Send heartbeat │
│ 7. Wait for next scan interval │
└─────────────────────────────────┘

Update detection logic

The registry package uses two strategies depending on the image tag:

  • Semver tags (e.g. v1.2.3, 1.25): compares semantic versions, reports when a newer version exists
  • Non-semver tags (e.g. latest, stable, main): compares image digests (SHA256), reports when the digest has changed

Authentication

  • Agent → Control plane: Bearer token (API key) issued at registration, stored hashed in PostgreSQL using argon2
  • Browser → Control plane: JWT session token, issued after login with ADMIN_TOKEN

Data flow: deployment

User clicks "Deploy" in dashboard

▼ (tRPC mutation)
Control plane creates DeployCommand record in DB

▼ (long-poll response)
Agent receives DeployCommand

├── Pull new image from registry
├── Update docker-compose.yml (if applicable)
├── docker compose up -d <service>
└── POST /api/v1/reports/commands/{id}/result


Control plane updates DB, sends notification email