GLOSSARY.md line-by-line audit. Eight corrections.
1. workspace-controller → environment-controller everywhere. The
controller reconciles the Environment CRD; "workspace" is banned as
a Catalyst scope, so it cannot be in a component name either. Fixed
in: GLOSSARY, ARCHITECTURE, PLATFORM-TECH-STACK, NAMING-CONVENTION,
SOVEREIGN-PROVISIONING, IMPLEMENTATION-STATUS, core/README,
BUSINESS-STRATEGY. Banned-term entry in GLOSSARY now explicitly
covers component names too.
2. "workspace repos" (per-Environment Gitea repos) → "Environment
Gitea repos" in GLOSSARY, PLATFORM-TECH-STACK.
3. JWT claim {workspace, org, role} → {environment, org, role} in
ARCHITECTURE projector diagram.
4. OpenOva definition refined: was "Never used to name a product",
which contradicted "OpenOva Catalyst", "OpenOva Cortex". Now: brand
prefix in product names; bare "OpenOva" = the company; bare
"Catalyst" = the platform.
5. Catalyst definition completed: was missing provisioning, billing,
gitea, observability — now lists all 14 control-plane components,
pointing at the table below.
6. Catalyst components table: added `provisioning` (validates
configSchema, commits to Environment Gitea); reordered to match
ARCHITECTURE §3 grouping; clarified each component's source-of-truth
(catalog-svc reads monorepo + Gitea, blueprint-controller watches
monorepo + Gitea, etc.).
7. Environment definition: refers to NAMING §2.4 for env_type values;
removed inline list that didn't match canonical ordering. Added
concrete examples (acme-prod, acme-dev, bankdhofar-uat).
8. Application example: dropped "RocketChat" which appeared nowhere
else; replaced with generic "running deployment" plus the
established WordPress / Postgres examples.
9. sovereign-admin description: was "runs Crossplane" — Crossplane is
platform plumbing not user-facing. Now: "manages the underlying
clusters via Crossplane (which is platform plumbing, not a
user-facing surface)".
Banned-term coverage:
- "Workspace" entry now covers BOTH the Catalyst scope AND component
naming (workspace-controller → environment-controller).
Refs #37
11 KiB
Catalyst Control Plane (core/)
The Go application that implements the Catalyst control plane — the user-facing UI and the controllers that turn a Kubernetes cluster into a Sovereign.
Status: Design + scaffolded. This directory currently contains the agreed structure as .gitkeep placeholders — Go code is yet to be written.
Updated: 2026-04-27.
Read first:
docs/GLOSSARY.md,docs/ARCHITECTURE.md,docs/IMPLEMENTATION-STATUS.md. This README assumes that context. The structure below describes the target layout once implementation begins.
What this is
A single Go application (the core/ directory) packaged as multiple components of the Catalyst control plane. The same codebase produces:
- console — the primary user-facing UI (Astro/Svelte/React frontend served by a Go backend).
- marketplace — the public-facing Blueprint card grid.
- admin — the
sovereign-adminoperations UI. - provisioning — the service that validates configSchema, composes manifests, commits to Environment Gitea repos.
- projector — the CQRS read-side service: NATS JetStream → KV → SSE.
- catalog-svc — Blueprint catalog API.
- environment-controller — reconciles the Environment CRD (vcluster + Flux + Gitea + webhook).
- blueprint-controller — watches Blueprint repositories.
- billing — per-Org metering.
These are deployed as separate workloads but share most of the codebase via internal packages.
What this is not
- It is not a "bootstrap wizard" + "lifecycle manager" duo. The historical split between those two is gone — both fold into the Catalyst control plane. Phase 0 bootstrap is performed by
catalyst-provisioner(an OpenOva-hosted service or a customer-deployed Blueprint) running OpenTofu; thereafter, the control plane runs continuously inside the Sovereign. - It is not Crossplane. Crossplane is an infrastructure dependency, deployed alongside.
- It is not Backstage. The Catalyst console is purpose-built for Catalyst's data model; we don't reuse Backstage's plugin system.
Target directory structure
This is the structure once implementation begins. Today, the apps/, internal/, pkg/, ui/, and deploy/ directories exist as .gitkeep placeholders only.
core/
├── apps/ # one binary per control-plane component
│ ├── console/ # console + marketplace + admin (frontend + Go backend)
│ ├── projector/ # CQRS projector service (NATS JetStream → KV → SSE)
│ ├── environment-controller/ # reconciles Environment CRD (vcluster + Flux + Gitea)
│ ├── blueprint-controller/ # watches Blueprint folders/repos, registers CRDs
│ ├── provisioning/ # validates configSchema, commits to Environment Gitea
│ ├── catalog-svc/ # serves Blueprint catalog API
│ └── billing/ # per-Org metering
├── internal/
│ ├── domain/ # Pure business logic, zero infra deps
│ │ ├── sovereign.go
│ │ ├── organization.go
│ │ ├── environment.go
│ │ ├── application.go
│ │ ├── blueprint.go
│ │ └── events.go
│ ├── application/ # Use cases / orchestration
│ ├── adapters/ # Infrastructure adapters
│ │ ├── kubernetes/
│ │ ├── crossplane/
│ │ ├── opentofu/ # bootstrap-only
│ │ ├── gitea/
│ │ ├── jetstream/ # NATS JetStream
│ │ ├── openbao/
│ │ └── keycloak/
│ └── events/ # CloudEvents envelopes
├── pkg/
│ └── apis/v1alpha1/ # CRD types: Sovereign, Organization, Environment, Application, Blueprint, EnvironmentPolicy, SecretPolicy, Runbook
├── ui/ # Frontend (Astro + Svelte; same codebase serves console / marketplace / admin via routes)
└── deploy/ # Kustomize bases for each control-plane component
The .gitkeep directories in this tree are deliberate — they pin the agreed layout while implementation work is scheduled. As each binary or package is written, its .gitkeep is removed and the corresponding row in docs/IMPLEMENTATION-STATUS.md flips from 📐 to ✅.
Legacy apps/bootstrap/ and apps/manager/ placeholders
The current filesystem also contains apps/bootstrap/ and apps/manager/ — empty directories from an earlier (now retired) split where bootstrap and lifecycle-management were modeled as separate binaries. These two folders will be removed when the new apps/ layout above is scaffolded; we keep them in the meantime to avoid spurious git rm churn before there's anything to replace them with.
CRDs (in pkg/apis/v1alpha1/)
apiVersion: catalyst.openova.io/v1alpha1
kind: Sovereign # the top-level deployment object
─────────────────────────
kind: Organization # multi-tenancy unit inside a Sovereign
─────────────────────────
kind: Environment # {org}-{env_type} scope; vcluster + Gitea repo
─────────────────────────
kind: Application # an installed Blueprint
─────────────────────────
kind: Blueprint # registered from a Blueprint repo
─────────────────────────
kind: EnvironmentPolicy # PR / soak / change-window rules per Env
─────────────────────────
kind: SecretPolicy # rotation rules
─────────────────────────
kind: Runbook # auto-remediation runbooks
See docs/ARCHITECTURE.md for how these compose, and docs/BLUEPRINT-AUTHORING.md for the Blueprint CRD spec.
Hexagonal architecture
┌───────────────────────┐
HTTP handlers ──►│ │──► Kubernetes API
K8s controllers ►│ Domain (pure Go) │──► OpenBao
JetStream subs ─►│ │──► Gitea
SSE clients ◄──│ │──► Crossplane
└───────────────────────┘
(zero external deps)
The domain layer is pure Go. All I/O goes through adapters in internal/adapters/. This keeps the core business logic (Sovereign, Organization, Environment, Application, Blueprint state machines) independent of any specific infrastructure choice — easy to test, easy to swap adapters when a backing technology evolves.
Event-driven core
// Domain emits events
type ApplicationInstallRequested struct {
Environment string
Name string
Blueprint BlueprintRef
Values ConfigValues
}
// Bus routes to handlers (in-process Go channels for fast path,
// JetStream for durable / cross-service fan-out)
bus.Subscribe(ApplicationInstallRequested{}, func(e Event) {
plan := planner.Compose(e)
git.Commit(e.Environment.GiteaRepo, plan.Manifests)
events.Publish("ws.<env>.application.installing", e)
})
The internal in-process bus and the JetStream spine use the same envelope shape (CloudEvents). Local tests run with the in-process bus; production runs with JetStream as the durable transport.
Reconciliation pattern (Manager components)
Standard controller-runtime style:
func (r *EnvironmentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var env catalystv1.Environment
if err := r.Get(ctx, req.NamespacedName, &env); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Ensure vcluster
if err := r.ensureVCluster(ctx, &env); err != nil {
return ctrl.Result{}, err
}
// Ensure Gitea repo
if err := r.ensureGiteaRepo(ctx, &env); err != nil {
return ctrl.Result{}, err
}
// Bootstrap Flux inside vcluster
if err := r.ensureFluxBootstrap(ctx, &env); err != nil {
return ctrl.Result{}, err
}
// Wire webhook
if err := r.ensureWebhook(ctx, &env); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
Per-CRD reconcilers live under apps/<controller>/internal/. Each is its own deployable component to keep blast-radius small (a bug in blueprint-controller cannot stall environment-controller).
User journeys (implementation references)
| Journey | Where it lands |
|---|---|
| Sovereign bootstrap | Phase 0 done by catalyst-provisioner; this codebase contains the OpenTofu modules under apps/provisioning/opentofu/ and the post-bootstrap Catalyst install logic. |
| Environment creation | environment-controller reconciles an Environment CR. |
| Application install | apps/provisioning/ validates and commits to the Environment's Gitea repo. Flux (in the vcluster) reconciles. |
| Promotion between Environments | apps/console/ opens a Gitea PR; EnvironmentPolicy controller gates merges. |
| Observability fanout | apps/projector/ consumes JetStream, writes JetStream KV, fans SSE to console clients. |
| Blueprint registration | apps/blueprint-controller/ watches Blueprint repos, validates, registers. |
For UX-level detail see docs/PERSONAS-AND-JOURNEYS.md.
Tech stack
| Layer | Technology |
|---|---|
| Language | Go 1.22+ |
| Web framework | Chi (HTTP) + connect-go (RPC) |
| K8s clients | controller-runtime, client-go |
| Frontend | Astro 5 + Svelte 5 + Tailwind 4 |
| Build | Ko (Go containers), Vite (UI) |
| Testing | go testing, Ginkgo for controllers, Playwright for UI |
| Telemetry | OpenTelemetry SDK (traces + metrics + logs) |
| Auth | JWT validation (Keycloak-issued) for users; SPIFFE SVID (transport mTLS) for workloads |
Local development
# Run console (frontend + Go backend)
cd core/apps/console
go run .
# Run projector
cd core/apps/projector
go run .
# Run a controller against a local kind cluster
cd core/apps/environment-controller
kind create cluster --name catalyst-dev
go run . --kubeconfig $HOME/.kube/config
# Frontend dev mode
cd core/ui
npm install
npm run dev
# Build all containers
ko build ./apps/...
Each apps/<x> folder is a separate Go module entrypoint; they share internal/ and pkg/ via the parent module.
Deployment
Each component has a Kustomize base under deploy/<component>/. The umbrella Blueprint bp-catalyst-platform (in products/catalyst/) composes all of them into a single deploy unit.
For Sovereign-level provisioning details see docs/SOVEREIGN-PROVISIONING.md.
Part of OpenOva Catalyst.