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
- One Helm chart that deploys a single demarkus world, parameterized for read-only mode, read-auth, storage class, resources, and image tag.
- One topology pattern (Argo CD
ApplicationSetover aworlds:list) that instantiates the chart N times to materialize a universe. - 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.
- 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
WorldCRD. 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:
StatefulSetwithreplicas: 1(sharing a content root across replicas is a Phase 7 problem; for now one server per world).volumeClaimTemplatesso each instance owns its own PVC. No sharedPersistentVolumeClaimresource — that has been the source of the PVC pain.Serviceof typeClusterIPexposing UDP/6309.LoadBalanceris opt-in via values.
Values surface (minimal):
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 generateonce per entry intokens.labels, writes the resultingtokens.tomlinto aSecretnamed<release>-tokens, and writes the raw tokens into a separateSecretnamed<release>-token-valuesfor 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
<release>-tokens. Re-running the Job afterhelm upgradeadds new labels without rotating existing ones. - Sends
SIGHUPto the StatefulSet pod (viakubectl rollout restartfallback) 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/patchonSecretandpods/execfor the SIGHUP path, scoped to the release namespace only.
Acceptance:
helm install demo ./deploy/helm/demarkus-server --set storage.className=standardproduces a healthy world reachable via the Service.kubectl execinto the pod andmark_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=writeradds a new token label and SIGHUPs without rotatingadmin. --set readOnly=truemakes the pod rejectPUBLISH/APPEND/ARCHIVEwithnot-permitted.
6.2 — Universe Topology (deploy/k8s/examples/)
Reference Argo CD ApplicationSet that iterates a worlds: list and instantiates the chart per entry.
# 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
Applicationresources. 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 <world>-tokens Secret,
SIGHUPs the world, returns { token, install_url }
DELETE /tokens/{label}
→ removes label from <world>-tokens, SIGHUPs
GET /me/install?world=<name>&label=<label>
→ returns a one-shot shell script (text/plain). Single-use, expires in 5 min.
The script writes ~/.config/demarkus/auth and patches ~/.claude.json.
GET /worlds
→ list of worlds the authenticated user is allowed to mint against,
derived from OIDC group → world mapping in broker config.
Implementation notes:
- Go service, single binary, reuses
tools/demarkus-tokenas a library (refactordemarkus-token's generate path intotools/internal/token/so both the CLI and broker call the same function). - Audit log to stdout:
{ts, sub, action, world, label, ok}. No separate audit DB; cluster log aggregation handles retention. - Per-user rate limit (in-memory token bucket) on
POST /tokensto prevent runaway minting. - Scope minted token paths/ops to the OIDC subject's group mapping. A user in
team-acan only mint tokens formark://team-a/*. Group → world mapping lives in broker config.
Acceptance:
curl -H "Authorization: Bearer <oidc>" -d '{"label":"alice","paths":["/**"],"ops":["read"]}' broker/worlds/team-a/tokensreturns a token that successfully readsteam-aand is rejected onteam-b.- Revocation (
DELETE) takes effect within one SIGHUP — the next read with the revoked token returnsnot-permitted.
6.4 — User Install Flow
The broker's GET /me/install returns a small shell script. Goals: idempotent, transparent (user can cat before piping), zero new dependencies on the user's machine.
Script behavior:
- Writes
~/.config/demarkus/auth(mode 0600) with the token. Existing auth file is preserved asauth.bak. - Reads
~/.claude.json(or creates it), adds an entry undermcpServers.demarkus-<world>pointing to the demarkus-mcp binary with the right--serverURL andDEMARKUS_AUTHenv injection. Leaves other MCP servers untouched. - Prints the next-step instruction: restart Claude Code, run
/soul.
Why not claude mcp add: the CLI works but assumes Claude Code is installed and on PATH. A direct JSON patch is more portable and doesn't depend on the CLI's surface staying stable. The script can fall back to claude mcp add if it detects the CLI.
Acceptance:
- Fresh machine:
curl -sSL <broker>/me/install?world=team-a | sh→ restart Claude Code →/soulliststeam-adocuments. - Re-running the install script does not duplicate the MCP entry.
- Removing the entry from
claude.jsonand re-running re-adds it.
6.5 — Documentation (soul)
/deployment.md(new): chart values reference, broker setup, OIDC configuration, RBAC, troubleshooting (PVC stuck pending, tokens not reloading, Argo sync drift). Linked from/index.mdunder a new "Deployment" section./plans/universe-deployment.md(this doc): the working plan, updated as sub-phases complete./journal.md: an entry per sub-phase landing with what shipped and what surprised us.
Risks and Open Questions
- Bootstrap Job RBAC. The Job needs Secret write + pod exec in its own namespace. Some clusters disallow exec from Jobs by org policy. Fallback:
kubectl rollout restartinstead ofpod execfor SIGHUP. Restart is heavier but uses only Deployment-level permissions. - Helm chart vs operator. ApplicationSet handles the static "spin up N worlds from a list" case. Dynamic per-world config (e.g., a UI that adds read-auth paths) would push toward an operator. Defer until that requirement is real.
- Broker secret-write blast radius. The broker holds K8s API credentials capable of writing token Secrets across all world namespaces. Mitigations: scope its ServiceAccount to a specific list of namespaces (one per world) via
RolenotClusterRole; emit audit log on every Secret write; consider sealed-secrets or external-secrets for the long-term. - OIDC provider coupling. First implementation will likely target one provider (GitHub or Auth0). Keep the provider behind an interface (
VerifierwithVerify(token) (sub, groups, err)) so adding a second provider is mechanical, not a rewrite. - Token revocation timing. SIGHUP reloads
tokens.tomlbut in-flight requests with the revoked token complete normally. Acceptable for a non-realtime revocation model; document it explicitly. - PVC migration / backup. The chart deliberately does not handle backup. Operator chooses: Velero, volume snapshots, or
demarkus-agent sync(Phase 5) to a replica world. Document the options, do not bake one in.
Sequencing
- 6.1 — chart lands first. Single world, real cluster (kind or k3d in CI), automated test that installs and reads.
- 6.2 — ApplicationSet lands second. Three-world reference universe in a CI cluster.
- 6.3 — broker prototype lands in
tools/demarkus-broker/. Validates the API shape against the chart. - 6.4 — install script lands with the broker.
- 6.5 — docs are updated incrementally as each sub-phase merges, not in a single doc PR at the end.
- Repo split for the broker happens after 6.4, once the API has been exercised end-to-end.
What's Explicitly Deferred to Phase 7+
- Multi-replica worlds with shared storage (RWX PVCs or object-store backend).
- Cross-cluster universe federation.
- Operator with a
WorldCRD. - A web UI on top of the broker (currently CLI-only via the install script).
- Managed/SaaS hosting.