- Wizard step canonical order updated to Org → Topology → Provider → Credentials → Components → Domain → Review (RUNBOOK-PROVISIONING, DEMO-RUNBOOK, IMPLEMENTATION-STATUS); SKU pickers cross-ref the PROVIDER_NODE_SIZES per-provider catalog (#176). - StepComponents UX rewritten: single flat marketplace card grid with family chips + product/family routes, two tabs (Choose Your Stack + Always Included) — replaces the prior "two-tab Mandatory infra/Apps" + "grouped by product header" prose (PRODUCT-FAMILIES, RUNBOOK- PROVISIONING, DEMO-RUNBOOK, COMPONENT-LOGOS). - CORTEX familyDependencies = [] reflected in PRODUCT-FAMILIES; the Specter / BGE cascade narratives rewritten to component-level-only resolution (langfuse → cnpg, librechat → ferretdb → cnpg) — fixes the "selecting Spector pulls entire FABRIC" over-broad claim. - catalyst-api OpenTofu workdir realigned from /var/lib/catalyst/... to /tmp/catalyst/tofu/<fqdn>/ via CATALYST_TOFU_WORKDIR env var (commit27527e4c) — fixes runtime drift in RUNBOOK-PROVISIONING, SOVEREIGN-PROVISIONING, DEMO-RUNBOOK; DEMO-RUNBOOK kubectl exec ns corrected from catalyst-system to catalyst. - Logo asset story rewritten: 58 logos (44 SVG + 14 PNG) sourced from CNCF artwork + project repos at #169b1d1c/#30ff318d, replacing the prior 62 stylised in-house marks; CI smoke-test (#6a7d2dd8) cross-referenced. - 12 G2 bootstrap-kit charts (original 11 + bp-powerdns #167) aligned in PROVISIONING-PLAN Group F + blueprint-release.yaml comment + SOVEREIGN-PROVISIONING header; previously stale at 11. - README repo-structure note updated: 12-component bootstrap kit + axon + external-dns leaf chart are built; 45 platform / 4 product folders remain README-only (was: "every folder except axon"). - ORCHESTRATOR-STATE main-tip SHA advanced fromdd578d1c→6afdb303with one-line summary of the post-Pass-1 batch. - VALIDATION-LOG: Reconcile Pass 2 entry appended (drift fixed across 10 files; six-category rubric). Reconcile Pass 2 against main @6afdb303— 10 files patched plus VALIDATION-LOG entry. Doc patches are landing first so the in-flight wizard step-reorder branch will merge into a doc set that already names the canonical order, avoiding a second drift round. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
Catalyst-Zero Provisioning Plan
Status: Authoritative working plan — execution underway. Updated: 2026-04-29. Owner: OpenOva engineering. Parent issue: #43. Sub-tickets: A–M groups, #45–#175. Post-Group-M continuation tickets (#161, #162, #163, #167, #168, #169, #170, #171, #173, #174, #175) extend the plan with the per-Sovereign PowerDNS zone model, pool-domain-manager + registrar adapters, three-mode StepDomain (pool/byo-manual/byo-api), the wizard StepComponents redesign, and k8gb retirement.
Execution status (live)
| Group | Tickets | Status | Commits |
|---|---|---|---|
| A — Code consolidation | 9 | ✅ Done | 3c2f7e4 |
| B — SME backend services | 10 | ✅ Source migrated; CI workflow live | 7646840 |
| C — Cutover Catalyst-Zero | 8 | ✅ Flux is now reconciling Catalyst-Zero from github.com/openova-io/openova (public repo) — confirmed via kubectl get gitrepository -A returning openova-public source serving the catalyst-platform Kustomization |
9d93912, dc56854, bd967a7, 61de3da, 9fdfe07, 8c40984 (Group C cutover merge) |
| D — Wizard | 10 | 🚧 Domain capture + Hetzner project ID added; AppsStep replacement pending | 854a063 |
| E — Provisioner backend | 13 | 🚧 Real Hetzner client + bootstrap installer + Dynadot DNS landed; SSH kubeconfig fetch is stub | 915c467, db4f21a, 07b4bcf |
| F — Bootstrap-kit Helm charts | 14 | ✅ All 12 G2 wrapper charts (original 11 + bp-powerdns #167) + blueprint-release CI live | 8c0f766, 0190c605 |
| G — DNS multi-domain | 6 | ✅ Superseded by PowerDNS authoritative (#167) + pool-domain-manager (#163) + registrar adapters (#170) — Dynadot is now one of five registrar adapters inside PDM, not the authoritative DNS surface | db4f21a, 0190c605 (#167), 2854d652 (#163), 567d7e1f (#170) |
| H — Franchise model | 7 | 🚧 docs/FRANCHISE-MODEL.md authored from existing admin impl; cross-Sovereign voucher deferred | this commit |
| I — Wizard UX | 6 | 📐 SSE event log pane + step indicator pending | |
| J — Hetzner infra | 6 | 🚧 cloud-init in repo; firewall + k3s flags wired into provisioner | 07b4bcf |
| K — Documentation | 8 | 🚧 IMPLEMENTATION-STATUS + core/README + products/catalyst/README updated; component-count anchor refreshed 53 → 56 (spire + nats-jetstream + sealed-secrets factored in); reconcile-pass-1 (2026-04-29) refreshed canonical docs against PowerDNS/PDM/registrar-adapter ground truth | 3c2f7e4, 8c0f766, group-k-docs, reconcile-pass-1 |
| L — Testing | 8 | 📐 Playwright + integration tests pending | |
| M — End-to-end DoD | 9 | 📐 Awaiting Hetzner credentials from operator + first OCI-artifact CI runs to complete |
This document captures the agreed plan for consolidating the existing nova/console/admin/marketplace code into the public OpenOva Catalyst monorepo, deploying it as Catalyst-Zero (the first Catalyst Sovereign — running on Contabo, the chicken in the chicken-and-egg problem), and then provisioning the first franchised Sovereign on Hetzner via the wizard at console.openova.io/sovereign.
This plan is the canonical reference. It supersedes any session-local conversation. Future Claude sessions or new contributors should read this first.
1. The chicken-and-egg problem and its resolution
Catalyst is a Kubernetes-native control plane that provisions other Sovereigns. Provisioning a Sovereign requires a provisioner service (catalyst-provisioner.openova.io per SOVEREIGN-PROVISIONING.md §2). That provisioner has to run somewhere. It cannot run inside the Sovereign it is provisioning (chicken-and-egg).
Resolution: the legacy nova/console/admin/marketplace stack currently running on Contabo k3s (in namespaces catalyst, sme, marketplace, website) is Catalyst-Zero — the first Sovereign. It exists today, has running pods today, and is the chicken from which the egg (the first Hetzner-hosted franchised Sovereign) gets provisioned.
The work in this plan consolidates that existing code into the public repo, redeploys it as a public-repo build (CI from github.com/openova-io/openova), and then uses it to provision the first franchised Sovereign. There is no greenfield "build Catalyst from scratch" — the Sovereign already exists; we are aligning it to the canonical Catalyst contract.
2. Current state inventory (verified against live cluster + repos, 2026-04-28)
2.1 Code locations (today)
| What | Where today | Where it must end up |
|---|---|---|
| Catalyst console UI (Astro+Svelte) | openova-private/apps/console/ |
openova/core/console/ |
| Catalyst admin UI (Astro+Svelte) | openova-private/apps/admin/ |
openova/core/admin/ |
| Catalyst marketplace UI (Astro+Svelte) | openova-private/apps/marketplace/ |
openova/core/marketplace/ |
| marketplace-api (Go backend) | openova-private/website/marketplace-api/ |
openova/core/marketplace-api/ |
| Catalyst-zero deployment chart | openova-private/clusters/contabo-mkt/apps/catalyst/ |
openova/products/catalyst/chart/templates/ |
| Vite scaffold for sovereign-wizard | openova/products/catalyst/bootstrap/ui/ |
merges into openova/core/console/src/pages/sovereign/ |
| CI workflows (6 of them) | openova-private/.github/workflows/{catalyst-build,marketplace-api-build,sme-{admin,console,marketplace,services}-build}.yaml |
openova/.github/workflows/ |
| Voucher / billing / tenants admin surface | openova-private/apps/admin/src/{components/BillingPage.svelte, lib/api.ts, pages/{billing,catalog,orders,tenants}.astro} |
openova/core/admin/... (carry forward unchanged) |
2.2 Live deployment on Contabo (verified via kubectl get all -A)
| Namespace | Pods running | Notes |
|---|---|---|
catalyst |
catalyst-api + catalyst-ui | 39 days uptime |
sme |
console + admin + marketplace | 5–6 days uptime |
marketplace |
marketplace-api | 13 days uptime |
website |
openova-website | live |
These pods are Catalyst-Zero. They stay running through Phases 1–2; Phase 2 is a rolling-update cutover to public-repo image builds.
2.3 Existing 5-step wizard (the "Components (5)" page reference)
The "Components (5)" the user referenced is the 5-step marketplace flow at openova-private/apps/marketplace/src/components/:
PlanStep → AppsStep → AddonsStep → CheckoutStep → ReviewStep
AppsStep is what gets replaced with the unified marketplace card grid (driven by the same bp-<x> Blueprint surface every Catalyst Sovereign uses).
2.4 Voucher mechanism (already implemented)
Lives in openova-private/apps/admin/:
src/components/BillingPage.svelte— voucher / billing UIsrc/lib/api.ts— voucher API clientsrc/pages/{billing,catalog,orders,tenants}.astro— admin pages
This is the canonical voucher implementation. Do not redesign. Read what's there, propagate to franchised Sovereigns, document in docs/FRANCHISE-MODEL.md.
3. Architectural agreements (from the design conversation, durable)
These agreements survive any context compaction and apply to every phase of the work below.
- Catalyst-Zero is the existing Contabo deployment. Not greenfield. The work is consolidate + cutover + extend, not rebuild.
- omani.works is the first Sovereign-provided subdomain pool (registered to the OpenOva Dynadot account). User dynamically picks
omantel.omani.worksduring provisioning. The wizard offers BYO domain (customer's own) or a Sovereign-pool subdomain (default). Multi-region setups are out of scope for the first run. - Existing admin voucher implementation is the source of truth. Do not propose new CRDs. Read the existing implementation, propagate it to franchised Sovereigns, document it.
- G2 quality only. Catalyst-curated wrapper Helm charts at
platform/<x>/chart/for every component in the bootstrap kit. No upstream-as-is shortcuts. No corner-cutting. The unified Blueprint contract fromBLUEPRINT-AUTHORING.md§1 is the standard. - No mocks. No iterations. No partial deliveries. Waterfall — every phase produces real, deployed, working artifacts.
- All product code is public. Per the build-minutes constraint, code moves to
openova/(the public monorepo) before any further development. CI runs in the public repo from this point onward. - The Vite scaffold at
products/catalyst/bootstrap/ui/merges intocore/console/src/pages/sovereign/. It does not become its own deployable. - Sovereign-provisioning wizard target URL:
console.openova.io/sovereign. Captured fields include domain (BYO or pool), Hetzner Cloud API token, Hetzner project ID, Hetzner region (runtime parameter, never hardcoded), plus the marketplace-style App selection. - The Hetzner region is a runtime parameter chosen by the wizard user. Never hardcoded anywhere in code.
- Dynadot is OpenOva's registrar of record for the pool domains. The
dynadot-api-credentialsK8s secret inopenova-systemis account-scoped and coversopenova.ioplusomani.works(and any other domain in the same Dynadot account). Post-#167/#170 Dynadot is not authoritative DNS for any Sovereign zone — bp-powerdns is. Dynadot is one of five registrar adapters PDM uses to (a) keep the OpenOva pool domains' parent-zone NS records pointing at OpenOva PowerDNS and (b) honourbyo-apiSovereigns whose customer happens to use Dynadot.
4. The 8-phase waterfall
Each phase produces one or more commits to openova/. Each commit is real working code, not scaffold. No phase is skipped, abbreviated, or deferred.
Phase 1 — Code consolidation (openova-private → openova)
What: git mv the 4 apps (console, admin, marketplace, marketplace-api) from openova-private to openova/core/. Move 6 CI workflows to openova/.github/workflows/. Move Catalyst-Zero deployment manifests from openova-private/clusters/contabo-mkt/apps/catalyst/ to openova/products/catalyst/chart/templates/.
Outputs:
openova/core/{console,admin,marketplace,marketplace-api}/populatedopenova/.github/workflows/{catalyst-build,marketplace-api-build,sme-*-build}.yamlopenova/products/catalyst/chart/templates/{api-deployment,api-service,ui-deployment,ui-service,ingress}.yamlopenova/products/catalyst/chart/Chart.yaml(new)- All import paths, image refs (
ghcr.io/openova-io/openova/{console,admin,marketplace,marketplace-api,catalyst-api,catalyst-ui}:<sha>) updated - VALIDATION-LOG entry: Pass 105
Commit message: feat(consolidation): move Catalyst-Zero apps + CI from openova-private to public monorepo
Phase 2 — Cutover Catalyst-Zero to public-repo build
What: Trigger first public-repo CI run, get :<sha> images into GHCR, roll the existing Contabo deployment to the new images. Catalyst-Zero is now built from the public repo. Delete legacy paths from openova-private (preserved in git history).
Outputs:
- GHCR images at
ghcr.io/openova-io/openova/{console,admin,marketplace,marketplace-api,catalyst-api,catalyst-ui}:<sha> - Contabo k3s pods rolled to new image SHAs
openova-privatecleaned of legacy paths- VALIDATION-LOG entry: Pass 106
Commit message: infra(cutover): Catalyst-Zero now built from public repo
Acceptance: kubectl describe pod on each rolled pod shows image: ghcr.io/openova-io/openova/.... Console at console.openova.io still loads. Brief rolling-update window (<60s).
Phase 3 — Sovereign-provisioning wizard
What: Build the wizard at core/console/src/pages/sovereign/ using the Vite scaffold. Replace the legacy 5-step marketplace flow's AppsStep with a unified marketplace card grid (driven by bp-<x> Blueprint surface). Add Sovereign-provisioning-specific fields:
- Domain: BYO (customer's own domain) or pool selection (default
omani.works→ user picks subdomain likeomantel,acme-bank, etc.) - Hetzner Cloud API token (capture, store via ESO into OpenBao, never log)
- Hetzner project ID
- Hetzner region (dropdown of valid Hetzner regions; runtime parameter)
- Sovereign owner email (becomes initial sovereign-admin)
- Initial App selection (the unified marketplace grid)
Outputs:
openova/core/console/src/pages/sovereign/index.astro+ sub-pages for each wizard stepopenova/core/console/src/components/sovereign/{DomainStep,HetznerStep,AppsStep-unified,ReviewStep}.svelte- The legacy bootstrap Vite scaffold at
openova/products/catalyst/bootstrap/ui/is merged in and the directory deleted (its content is now part ofcore/console/) - VALIDATION-LOG entry: Pass 107
Commit message: feat(console): sovereign-provisioning wizard at /sovereign with domain + Hetzner inputs + unified marketplace App selection
Phase 4 — Provisioner backend
What: Build the wizard's backend at products/catalyst/bootstrap/api/ (the Go service deployed as catalyst-api in the catalyst namespace on Catalyst-Zero). Real backend that takes wizard input → calls OpenTofu → returns Sovereign provisioning state via SSE. Per INVIOLABLE-PRINCIPLES.md #3, no cloud APIs are called from Go directly — OpenTofu owns Phase 0, Crossplane owns day-2, and Hetzner client code is reserved for read-only credential validation.
Outputs:
products/catalyst/bootstrap/api/internal/provisioner/— thin wrapper aroundtofuthat writestofu.auto.tfvars.jsonfrom validated wizard input, runstofu init && tofu plan && tofu apply -auto-approve, streams stdout/stderr lines to the wizard via SSEproducts/catalyst/bootstrap/api/internal/hetzner/— read-only Hetzner client for credential validation (POST /api/v1/credentials/validate); never used to mutate cloud stateproducts/catalyst/bootstrap/api/internal/pdm/— PDM client (/v1/reserve,/v1/commit,/v1/validate) for pool-subdomain allocation and registrar-token validationproducts/catalyst/bootstrap/api/internal/dynadot/— Dynadot client (used as one registrar adapter inside PDM's adapter set, not for direct DNS writes from this service)products/catalyst/bootstrap/api/internal/handler/— REST handlers includingPOST /api/v1/deployments,GET /api/v1/deployments/{id}/logs(SSE),POST /api/v1/deployments/{id}/phases/{phase}/retry,POST /api/v1/credentials/validate,POST /api/v1/subdomains/check,GET /api/v1/registrarsinfra/hetzner/main.tf— OpenTofu module (network, firewall, ssh-key, control-plane + worker servers, load balancer)- VALIDATION-LOG entry: Pass 108
Commit message: feat(provisioner): real Hetzner Sovereign provisioning end-to-end
Phase 5 — Bootstrap kit Helm charts (G2 quality)
What: Real Catalyst-curated wrapper Helm charts at platform/<x>/chart/ for every bootstrap-kit component. Each chart wraps upstream OSS with Catalyst-specific values, includes a blueprint.yaml per the unified Blueprint contract from BLUEPRINT-AUTHORING.md §1, publishes a bp-<name>:<semver> OCI artifact via CI fan-out.
Components (in dependency order):
platform/cilium/chart/(CNI must come first)platform/cert-manager/chart/platform/flux/chart/(host-level)platform/crossplane/chart/platform/sealed-secrets/chart/(transient bootstrap-only)platform/spire/chart/(theplatform/spire/folder may need to be added — workload identity)platform/nats-jetstream/chart/(theplatform/nats-jetstream/folder may need to be added)platform/openbao/chart/platform/keycloak/chart/platform/gitea/chart/products/catalyst/chart/— the umbrellabp-catalyst-platform
Outputs:
- 11 directories with
Chart.yaml,values.yaml,templates/,blueprint.yaml, optionalcompositions/,policies/,overlays/ - 11 entries in
openova/.github/workflows/blueprint-release.yaml(path-matrix CI fan-out) - 11 OCI artifacts published at
ghcr.io/openova-io/bp-<name>:<semver>after first CI run - One commit per chart (11 commits) — incremental review possible
- VALIDATION-LOG entries: Pass 109 through Pass 119
Commit messages: feat(bp-<name>): G2 Catalyst-curated chart for <name> per BLUEPRINT-AUTHORING contract
Phase 6 — DNS architecture: PowerDNS authoritative + PDM + registrar adapters
What: The DNS architecture has two layers. Authoritative DNS lives on bp-powerdns (#167) — every Sovereign zone (pool: omantel.omani.works, BYO: acme.bank.com) gets its own PowerDNS zone with DNSSEC + lua-records. Allocation + registrar control lives on the pool-domain-manager service (#163), which exposes registrar adapters (#170) for byo-api flow:
- Pool subdomains (e.g.
<sub>.omani.works,<sub>.openova.io): PDM/v1/reservechecks availability,/v1/commitcreates the per-Sovereign PowerDNS zone, writes the canonical 6-record set, and updates the parent zone's NS delegation via the OpenOva Dynadot registrar adapter. - BYO with manual NS-flip (
byo-manual): wizard surfaces the OpenOva NS list; customer pastes them into their own registrar UI; catalyst-api polls until propagation; PDM/v1/committhen writes the canonical record set into the new PowerDNS zone (no parent-zone change from OpenOva). - BYO with API NS-flip (
byo-api): customer picks their registrar from the supported list (Cloudflare, Namecheap, GoDaddy, OVH, Dynadot — #170), pastes a token; PDM/v1/validateconfirms scope read-only; on commit, the matching registrar adapter flips the NS records to OpenOva's NS set.
Outputs:
core/pool-domain-manager/— Go service deployed atpool-domain-managerinopenova-system, CNPG-backedpdm-pg. Modules:internal/allocator,internal/pdns,internal/registrar,internal/dynadot,internal/reserved,internal/store. CI:.github/workflows/pool-domain-manager-build.yaml.platform/crossplane/compositions/composition-pool-allocation.yaml+ matching XRD — declarative Crossplane wrapper around PDM/v1/reserveso Sovereign provisioning runs through the canonical IaC path.platform/powerdns/— bp-powerdns wrapper chart (Chart.yaml, values.yaml, blueprint.yaml, templates) with DNSSEC + lua-records on by default, dnsdist companion for rate-limiting.- VALIDATION-LOG entry: Pass 120 (component-count refresh + PDM landing).
Commit message: feat(dns): bp-powerdns + pool-domain-manager + registrar adapters for pool/byo flows
Phase 7 — Franchise model docs + voucher propagation
What: Read existing voucher implementation in admin app. Write docs/FRANCHISE-MODEL.md documenting it as canonical. Ensure the new Sovereign at omantel.omani.works has its own admin surface (the same admin app, deployed inside the Sovereign) where omantel-admin can issue vouchers to omantel's tenants. Update GLOSSARY.md with Voucher and Franchisee definitions if not already present.
Outputs:
openova/docs/FRANCHISE-MODEL.md— canonical doc- Updates to
GLOSSARY.mdif needed - Updates to
BUSINESS-STRATEGY.mdrevenue model if needed - VALIDATION-LOG entry: Pass 121
Commit message: docs(franchise): canonical franchise model + voucher propagation, sourced from existing admin impl
Phase 8 — End-to-end provisioning (live demo / DoD)
What: From browser at console.openova.io/sovereign:
- User logs in (Keycloak SSO)
- Picks "New Sovereign"
- Pastes Hetzner Cloud API token + project ID, picks region (any — runtime parameter)
- Picks domain: pool →
omani.works→ user typesomantel(createsomantel.omani.works) - Picks initial Apps (unified marketplace selection)
- Click Provision
- Watches SSE-driven progress for ~10 minutes
- Provisioning completes; new Sovereign at
omantel.omani.worksis reachable - omantel-admin (initial sovereign-admin) logs into
console.omantel.omani.works - omantel-admin issues 1 voucher
- A fictional customer redeems the voucher at
omantel.omani.works/redeem?code=... - Customer's Organization + Environment + first App is created on omantel.omani.works
- Customer reaches their App's URL
Acceptance: every step above works without intervention. No mocks, no manual steps beyond the browser clicks.
Outputs:
- VALIDATION-LOG entry: Pass 122 — DoD documented with screenshots / kubectl evidence
- Optional:
docs/DEMO-RUNBOOK.mdfor repeatability
5. What this plan does NOT change
- The unified Application = Gitea Repo model (Pass 103) is preserved everywhere. The franchised Sovereign at omantel.omani.works will use the same model — one Gitea Org per Catalyst Organization, one Gitea Repo per Application.
- The 5 conventional Gitea Orgs convention (
catalog,catalog-sovereign,<org>per Catalyst Organization,system) applies to the new Sovereign exactly as it does to Catalyst-Zero. - The component-count anchor (Pass 104 set 53; Pass 105 raised it to 56 with spire + nats-jetstream + sealed-secrets) holds. SeaweedFS unified S3 encapsulation, Guacamole in bp-relay, OpenBao independent-Raft per region — all preserved.
- The audit procedure stays on-demand (no scheduled loops). The
audit-catalyst-docsskill is the only validation entry point.
6. References
docs/ARCHITECTURE.md— target architecture (the design Catalyst-Zero is being aligned to)docs/SOVEREIGN-PROVISIONING.md§3 Phase 0 — bootstrap kit dependency order (canonical reference for Phase 5 of this plan)docs/BLUEPRINT-AUTHORING.md§1 — unified Blueprint shape (the contract Phase 5 charts must satisfy)docs/IMPLEMENTATION-STATUS.md— gets updated incrementally as each phase lands (📐 → 🚧 → ✅)docs/AUDIT-PROCEDURE.md— how to validate after each phasedocs/VALIDATION-LOG.mdPass 1–104 — historical record; Pass 105+ tracks this plan's execution
Part of OpenOva