# Lucity Open-source PaaS on Kubernetes with full ejectability. Monorepo with a single Go control-plane binary plus a Vue 3 dashboard. ## Project - **Go workspace**: `go.work` with multi-module layout (Go 1.27) - **Module path**: `github.com/zeitlos/lucity` - **Monorepo**: `services/conductor` (control plane), `services/stagehand` (workload changes nobody asked for: plan-based placement), `services/cashier` (usage metering), `services/exporter` (external usage metrics), `services/dashboard` (Vue), `services/switchboard` (chat apps for agents), `pkg/` (shared Go), `charts/` (Helm) - **Platform images**: `ghcr.io/zeitlos/lucity/{conductor,stagehand,cashier,exporter,dashboard,docs,switchboard}` - **User workload images**: Zot (self-hosted OCI registry, `localhost:5000` in dev) - **Coding rules**: see `.claude/rules/` for architecture, Go, frontend, GraphQL, marketing, and working conventions ## Build & Run - **Conductor**: `go run ./cmd/conductor/...` from `services/conductor/`, or `make dev-conductor` - **Cashier**: `go run ./cmd/cashier/...` from `services/cashier/`, or `make dev-cashier` - **Stagehand**: `go run ./cmd/stagehand/...` from `services/stagehand/` - **Exporter**: `go run ./cmd/exporter/...` from `services/exporter/` (needs the `OVH_*` env vars) - **Dashboard**: `npm run dev` from `services/dashboard/` - **Build all**: `make build` - **GraphQL codegen**: `go generate ./internal/api/graphql/resolver.go` from `services/conductor/` ## Architecture ### Stateless Design The platform has no central database. All state is derived from external systems: - **Kubernetes**: namespaces, labels, Helm release state (Secrets), operator CRDs - **OCI Registry (Zot)**: built images, tags, digests - **Identity Provider (OIDC/Logto)**: users, roles, authentication, workspace metadata The platform is non-intrusive — its downtime does not affect running workloads. ### Single-Repository Model - **User's source repo** (GitHub): read-only to the platform, never written to - **No GitOps repo**: the conductor deploys workloads as standard Helm releases applied imperatively (Helm SDK). Deployment config is the chart values it computes; Helm stores the release state in-cluster. ### Multi-Tenant (Workspaces) Each Lucity instance supports multiple workspaces. A workspace is the tenant boundary — it owns projects, environments, and members. Workspace context is derived from the JWT token (workspace claim). Kubernetes namespaces carry `lucity.dev/workspace` labels for discovery. ### Services | Service | Port | Protocol | Purpose | |---------|------|----------|---------| | Conductor | 8080 (HTTP), 9004 (webhook HTTP), 9008 (metrics) | HTTP | Unified control plane: GraphQL, Helm release management for user changes, build orchestration, GitHub webhook receiver, the Stripe billing a user triggers (trial, plan selection, checkout, portal). Runs no background loops | | Stagehand | 9010 (metrics) | Kubernetes + Stripe (read) + Logto | Every background loop: build/deploy admission (event-driven), namespace scaffolding, chart upgrades, plan-based placement (dry-run capable), billing + abuse suspension (computed every pass, no webhooks), custom-domain verification + edge sync, backup recovery floors | | Cashier | 9009 (metrics) | Stripe + VictoriaMetrics + Kubernetes (read) | Usage metering into Stripe meter events, monthly plan credits | | Exporter | 9007 (HTTP) | Prometheus `/metrics` | Usage from external providers (object storage sizes) as metrics, scraped by the node-local otel agent | | Dashboard | 5173 | HTTP | Vue 3 SPA | | Switchboard | none, outbound only | Telegram Bot API + Mattermost + runner API | Chat apps of every workspace: Telegram and Mattermost bots bridged to agent runners, approvals in the chat. Reads chat apps and agent channels from its own namespace | ### Conductor internal layout ``` services/conductor/ ├── cmd/conductor/ # main, config, OIDC login, GraphQL + webhook servers ├── internal/ │ ├── api/ │ │ ├── graphql/ # gqlgen schema, resolvers, directives, models │ │ └── webhook/ # GitHub webhook receiver (github/, http/) │ ├── conductor/ # Client facade tying the domain packages together │ ├── buildjob/ # build Job orchestration (kubernetes/) │ ├── planner/ # source detection + build planning (railpack/) │ ├── source/ # user source repo access (github/) │ ├── directory/ # user/workspace directory (logto/) │ ├── dbquery/ # database explorer query execution │ └── billing/ # Stripe billing a user triggers (trial, plans, checkout, portal) ``` ### Communication - **Dashboard ↔ Conductor**: GraphQL over HTTP (port 8080) - **No service calls another**: conductor, stagehand and cashier share state through Stripe, Logto and the Helm releases only - **GitHub → Conductor**: HTTP webhooks (port 9004) - **Long-running operations**: polling (watch registry for images, poll Helm/Kubernetes for rollout status) ### Shared Packages (`pkg/`) graceful (server lifecycle), logger (slog + tint), deployer (Helm release apply + lucity-app values, shared by conductor and stagehand), platform (IDs + value types; kubernetes/ = discovery from Kubernetes), environment (namespace lifecycle), jobs + pipeline (build/deploy Job cache and admission), hostname (custom-domain DNS verification), edge (Bunny CDN), domains (custom-domain attach + edge sync), resources, image, usererror, billing (Stripe subscription lookup, plans, standing), auth (OIDC/JWT), labels (K8s label constants), tenant (workspace context), github (App + OAuth), logto (Logto Management API), to (pointer/conversion helpers). ## Feature Development Workflow 1. **Research** — how do Railway, Heroku, Render, Fly.io handle this? 2. **Architecture fit** — does it respect stateless design? Is it ejectable? 3. **Day-2 operations cost** — can a small team run it? 4. **Design APIs** — GraphQL schema 5. **Design deployment values** — how does this affect the lucity-app chart values? 6. **Design frontend** — Vue pages, composables, GraphQL queries 7. **Implement minimal** — ship the smallest useful version first 8. **Test** — GraphQL playground, dashboard end-to-end, Go unit tests 9. **Iterate** — extend with more advanced capabilities ## Smoke Testing ```sh make dev # conductor + cashier + dashboard with hot reload ``` Then: - GraphQL playground: http://localhost:8080/playground - Dashboard: http://localhost:5173/ - Webhook receiver: POST to http://localhost:9004/webhooks/github Logs land in `tmp/logs/{conductor,cashier,dashboard}.log`. ## Known Issues _None yet — this is a fresh scaffold._