# Plan: Universe Deployment (Phase 6) Deploy a demarkus universe — many worlds, optional hubs, optional souls — onto Kubernetes with declarative GitOps and self-service token distribution. Phase 5 (the demarkus agent, read auth, security hardening) gave a single hardened server. Phase 6 makes it cheap to stand up *N* of them and to onboard real users without hand-distributing tokens. ## Goals 1. **One Helm chart** that deploys a single demarkus world, parameterized for read-only mode, read-auth, storage class, resources, and image tag. 2. **One topology pattern** (Argo CD `ApplicationSet` over a `worlds:` list) that instantiates the chart N times to materialize a universe. 3. **One token broker** that mints scoped tokens via OIDC login and emits a one-shot install script that wires the user's local MCP config. 4. **Zero changes to protocol or core server.** Phase 6 is packaging and lifecycle, nothing else. If a need for a new core primitive surfaces, stop and discuss before landing. ## Non-Goals - Operator pattern with a `World` CRD. Argo CD ApplicationSet covers the lifecycle without a controller. Revisit only if per-world dynamic config from a UI becomes a real requirement. - Multi-cluster federation. A universe is a set of worlds in one cluster. Cross-cluster sync is a Phase 5 (`demarkus-agent sync`) concern. - Hosted SaaS. The chart and broker are for self-hosted clusters. A managed offering is a separate product question. - Non-markdown content. Server scope is unchanged. ## Repository Layout **Chart and reference manifests live in the monorepo.** They are tightly coupled to server flags, env vars, paths, and the `demarkus-token` binary. Splitting them out invites silent drift. ``` deploy/ helm/ demarkus-server/ # the chart Chart.yaml values.yaml templates/ statefulset.yaml service.yaml bootstrap-job.yaml secret-tokens.yaml configmap-readauth.yaml rbac.yaml k8s/ examples/ applicationset.yaml # Argo CD ApplicationSet over a worlds: list kustomize-overlay/ # hand-rolled alternative ``` **Broker starts in `tools/demarkus-broker/` for the prototype.** Coupled iteration on the broker API and the chart's Secret schema is much faster in one repo. Split to `latebit-io/demarkus-broker` once the API stabilizes (post-6.3) for the same reasons we split the Obsidian plugin: independent release cadence, separate issue tracker, isolated threat model. ## Sub-Phases ### 6.1 — Helm Chart (`deploy/helm/demarkus-server/`) A single chart that deploys one world. **Workload shape:** - `StatefulSet` with `replicas: 1` (sharing a content root across replicas is a Phase 7 problem; for now one server per world). - `volumeClaimTemplates` so each instance owns its own PVC. No shared `PersistentVolumeClaim` resource — that has been the source of the PVC pain. - `Service` of type `ClusterIP` exposing UDP/6309. `LoadBalancer` is opt-in via values. **Values surface (minimal):** ```yaml image: repository: ghcr.io/latebit-io/demarkus-server tag: "" # defaults to chart appVersion storage: className: "" # required, fail fast if empty size: 5Gi accessMode: ReadWriteOnce readOnly: false # sets DEMARKUS_READ_ONLY readAuth: enabled: false paths: [] # path globs that require read tokens tokens: bootstrap: true # run the Job; false if you manage tokens externally labels: # initial token labels to mint - name: admin paths: ["/**"] ops: ["publish", "read"] resources: {} # standard k8s shape tls: certSecret: "" # opt-in mount ``` **Bootstrap Job:** - Runs `demarkus-token generate` once per entry in `tokens.labels`, writes the resulting `tokens.toml` into a `Secret` named `-tokens`, and writes the *raw tokens* into a separate `Secret` named `-token-values` for the broker to read and revoke. Hashes vs raw values are kept in different Secrets so the server only ever mounts the hashes. - Idempotent: skips entries whose label already appears in `-tokens`. Re-running the Job after `helm upgrade` adds new labels without rotating existing ones. - Sends `SIGHUP` to the StatefulSet pod (via `kubectl rollout restart` fallback) so the server reloads tokens without a full restart. The fallback is fine because the Job is rare (chart install, label add). **RBAC:** - ServiceAccount for the bootstrap Job with `get/list/create/patch` on `Secret` and `pods/exec` for the SIGHUP path, scoped to the release namespace only. **Acceptance:** - `helm install demo ./deploy/helm/demarkus-server --set storage.className=standard` produces a healthy world reachable via the Service. - `kubectl exec` into the pod and `mark_fetch /` returns the seeded content (or empty list — chart does not seed content; that is a separate concern). - Re-running `helm upgrade --set tokens.labels[1].name=writer` adds a new token label and SIGHUPs without rotating `admin`. - `--set readOnly=true` makes the pod reject `PUBLISH/APPEND/ARCHIVE` with `not-permitted`. ### 6.2 — Universe Topology (`deploy/k8s/examples/`) Reference Argo CD `ApplicationSet` that iterates a `worlds:` list and instantiates the chart per entry. ```yaml # pseudo generators: - list: elements: - { name: team-a, paths: "/**", readAuth: false } - { name: team-b, paths: "/private/**", readAuth: true } - { name: hub, paths: "/**", readAuth: false, role: hub } template: metadata: { name: '{{name}}' } spec: source: repoURL: https://github.com/latebit-io/demarkus path: deploy/helm/demarkus-server helm: valuesObject: tokens: labels: ... readAuth: ... ``` Also ship a Kustomize overlay (`deploy/k8s/examples/kustomize-overlay/`) for clusters without Argo CD. Same chart, instantiated per directory. The `role: hub` flag does not change the chart — a hub is just a world that the `demarkus-agent` (Phase 5) crawls and publishes indexes to. The role label exists only as documentation/selector convenience. **Acceptance:** - Apply the ApplicationSet to an Argo CD instance pointing at the demarkus repo. Three worlds materialize as three Argo CD `Application` resources. Deleting an entry from the list removes the world. - README walks an operator from zero to a working three-world universe in under 15 minutes. ### 6.3 — Token Broker (`tools/demarkus-broker/`, prototype) Stateless HTTP service. Auth via OIDC (configurable provider). Persistence is K8s Secrets — no DB. **Endpoints (minimal):** ``` POST /worlds/{name}/tokens body: { paths: [...], ops: [...], label: "alice-laptop" } → mints token, calls demarkus-token, writes hash to -tokens Secret, SIGHUPs the world, returns { token, install_url } DELETE /tokens/{label} → removes label from -tokens, SIGHUPs GET /me/install?world=&label=