Plan: Universe Deployment
Ship the enterprise-grade deployment for demarkus. The deliverable is the deployment: a customer's ops team installs a Helm chart, runs a broker, runs an agent, and has a federated demarkus universe their org can use. Any company evaluating demarkus (their internal "POC") installs the same product an established customer runs in production. There is no separate "POC slice" — the slice mentality is rejected. We build it once, right, and customers trial the real thing.
Goal
Deliver a complete, supportable, production-grade Kubernetes deployment package for demarkus, including:
- A Helm chart for
demarkus-server(one world). ✓ - A Helm chart for
demarkus-broker(OIDC token issuance + revocation). ✓ - A Helm chart for
demarkus-agent(hub aggregator, crawl-and-index). ✓ - Reference topology examples (Argo CD
ApplicationSet, Kustomize overlay). - Backend-agnostic observability — structured slog emission + reference configs for Datadog, OTel Collector, Vector, Fluent Bit, Grafana Alloy.
- Customer-facing documentation — installation, security/threat model, operations, upgrade path, per-provider OIDC setup, observability recipes.
- A release pipeline producing images and chart releases consumable from GHCR. ✓
The same artifacts power first-customer trial and steady-state operations.
Non-Goals (Phase 7+ territory)
- Multi-replica worlds with shared storage (RWX / object backend).
- Cross-cluster universe federation.
- Operator with a
WorldCRD. - Hosted / managed SaaS.
- Non-markdown content.
Constraints
- No core protocol/server changes. Period. Per prior precedent (Claude Code plugin, Obsidian plugin,
feedback_plugin_scope.md). Observability is achieved by log derivation in a collector, not by adding/metricsor OTel SDK calls to the server. Note: in Slice A we promotedHashTokenand the token-mint library toprotocol/(underprotocol/auth.goandprotocol/token/). These are additive helper relocations consumed by server + CLI + future broker; no wire-protocol or server behavior changed. Slice C.2 addedprotocol/token.ParseBytesin the same additive spirit — a read-side helper so the broker's drift sweeper can inspect a world'stokens.tomlpayload via the map shape rather than substring matching on serialized TOML. §6.7.0 hoistedserver/internal/store/→protocol/store/in the same additive spirit — the disk-shape of versioned content lives at the protocol layer alongsidetoken/and theHashTokencontract; no API change, four import-path rewrites. - Capability-based auth model is non-negotiable. The server never learns identity — only labels.
- Markdown-only scope is non-negotiable.
Decisions (resolved during planning)
- Health probes use
exec, not HTTP. Both charts ship liveness/readinessexecprobes that invoke thedemarkusCLI fetching/.well-known/agent-manifest.md(always public per/architecture.md). No core change. Same pattern asredis-cli ping/pg_isready. - UDP port is a values knob. Default
6309, override viaserver.udpPort. Documented option: switch to443for VPN/middlebox-hostile networks (Cloudflare Warp Zero Trust, corp firewalls that filter non-standard UDP). Protocol default stays6309. - Wildcard TLS via cert-manager DNS-01. Single
*.<root>cert covers all worlds + broker hostnames. HTTP-01 cannot work for worlds (no HTTP); DNS-01 is the standard path for QUIC services. - DNS topology. One A record per world + one for the broker, all under the same wildcard zone. Each world exposed by
Service: LoadBalancerprotocol: UDP; broker by standard Ingress (HTTPS). - Hub aggregator is the existing
demarkus-agentatclient/cmd/demarkus-agent/. Already implementscrawl+daemonsubcommands with TOML config, multi-worker crawl, per-host token auth, per-server + aggregated index publishing. Verified end-to-end on 2026-05-11. Phase 5 is no longer a blocking prerequisite. demarkus-agentlives inclient/cmd/, nottools/. It's a protocol client (usesfedcrawl,fetch,tokens,links), not a utility. Memory rule sharpened:client/cmd/= protocol clients (CLI, TUI, MCP, agent);tools/= utilities (broker, future sync/ops binaries).- Token-mint library lives at
protocol/token/, nottools/internal/token/(revised mid-Slice-A after CodeRabbit review).protocol/is reachable by bothserver/(wheredemarkus-tokenCLI lives) andtools/(where the broker will live), whichtools/internal/was not.protocol/already owns theHashTokenbyte-shape contract, so it's the natural home for the on-disk token primitives that must round-trip identically across CLI mints and broker mints. demarkus-tokenanddemarkus-publishCLIs live attools/(§6.7.A merged 2026-05-13 PR #121).demarkus-tokenhad been a clean candidate since Slice A (only importsprotocol/token).demarkus-publishfollowed once §6.7.0 (PR #120) hoistedserver/internal/store/→protocol/store/, removing the server-internal dependency. Both ship as standalone binaries viatools/.goreleaser.ymlarchives (§6.7.B PR #123); they are NOT bundled into any runtime image.- Multi-OIDC. Broker speaks generic OIDC, not Google-specific. Provider behind a
Verifierinterface. Google validated first; Okta, Entra ID, Auth0 follow with config only. - Workload Identity (GKE) as a broker option. Values flag annotates the broker ServiceAccount with the GSA mapping. Off by default; on for GKE customers wanting no static SA keys. Not applicable to world servers.
- Broker is HA. Multi-replica with
resourceVersionoptimistic concurrency on Secret writes, retry on conflict. - Observability is log-derived, backend-agnostic. demarkus already emits structured slog. Charts ship Datadog autodiscovery annotations + reference configs for OTel Collector, Vector, Fluent Bit, Grafana Alloy. Customer's SRE picks the agent/backend. Latency histograms deferred — depends on what current slog includes per request; revisit during trials if needed.
- Backup/DR is documented, not built. The chart deliberately does not run backup CronJobs. Operations doc covers Velero, VolumeSnapshot, and
demarkus-agent syncas DR options. - Image hosting:
ghcr.io/latebit-io/{demarkus-server,demarkus-broker,demarkus-agent}. Three separate images, one per service. Released by §6.7.B per-module tag flow. - Chart registry:
oci://ghcr.io/latebit-io/charts/{demarkus-server,demarkus-broker,demarkus-agent}. Three OCI helm charts released by §6.7.B per-module tag flow alongside their images. - Cosign signing: deferred to backlog. Half-day CI add when wanted.
- Groups-claim case sensitivity (revised mid-Slice-C.1 after CodeRabbit back-and-forth). Group names match case-insensitively:
AllowConfig.Groupsis lowercased+trimmed at config load (same asDomainsandEmails) andgroupsMatchlowercases the claim's groups before compare. The validated IdP set (Google, Okta, Entra ID, Auth0) enforces case-insensitive group-name uniqueness, so distinct case-variant groups can't exist; a case-sensitive compare would silently fail when an operator writes "Engineering" but the IdP emits "engineering" after upstream normalization. Keycloak with deliberately distinct case-variant groups is out of scope; revisit if a customer asks. Domains and emails get the same lowercase-at-load + plain==treatment for symmetry. - Sweeper interval capped at 24h (Slice C.2). The
defaultToken.expiresAftertypical is 24h; a sweep interval longer than that would let expired tokens linger past their ExpiresAt for up to a whole sweep cycle, defeating the short-lived-tokens identity-lifecycle model (§Revocation §3).validate()rejects withsweeper.interval must be <= 24h. Lease-election timings (15s lease / 10s renew / 2s retry) intentionally not exposed in YAML — the client-go defaults match our failover budget and adding knobs invites misconfiguration. Revisit if a customer needs sub-15s failover. sweeper.disabledrather thansweeper.enabled(Slice C.2). YAML zero-value (false) is the production-safe default so an operator who omits the block gets the sweeper running. Naming the field positively (Enabled) would have made omission silently disable the janitor — exactly the wrong default for multi-replica deployments where expired tokens accumulate forever without it.- Rotate is re-login semantics, not refresh (Slice C.3).
POST /tokens/:label/rotatere-runsworldAllows(world.Allow, claims)on every call, so a user removed from the world's allowlist between mint and rotate can NOT extend access via rotation. The bearer ID-token verification at the HTTP layer already proves the IdP identity is still active; the per-world predicate re-check catches in-org permission changes (group removed, domain renamed, email taken off carve-out). Picked over rotation-as-refresh because rotation extends the lifetime — without re-auth, a user whose IdP account stayed live could rotate forever even after losing world-scope authorization. - Rotate scope vs. lifetime asymmetry (Slice C.3). Scope (paths + operations) stays frozen to the issuance record on rotate: operator narrowing of
DefaultToken.Pathsbetween mint and rotate does NOT shrink the rotated token's reach, because the user shouldn't be surprised by reduced access mid-session — they re-login to pick up new scope. Lifetime resets tonow + DefaultToken.ExpiresAfterfrom the operator's CURRENT config: operator tightening of expiry DOES apply on rotate, because shorter lifetimes are an explicit security-tightening lever and rotation must not bypass it. The asymmetry is intentional and pinned byTestRotateLabelPreservesIssuanceScope+TestRotateLabelLifetimeUpdatesToCurrentConfig. - Rotate sequence: mint new, then revoke old (Slice C.3). On revoke failure the new token is returned anyway with a wrapped soft error; the sweeper retires the orphan old label on expiry (or via drift if an operator hand-cleans). Picked over revoke-then-mint because that alternative leaves the user with no token if mint then fails, forcing a full re-login — worse UX than a brief two-valid-tokens window for the same user. Mirrors the Slice B partial-mint convention:
(MintResult{Label:""}, err)for hard failures,(MintResult{non-empty}, err)for soft partial success. - Middleware-not-handler auth + rate limit (Slice C.4). The three authed
/tokensroutes go throughrequireAuth → subjectRateLimit → handler;/auth/logingoes throughipRateLimit → authLogin;/auth/callback,/healthz,/readyzstay middleware-free. The old per-handlers.authenticate(w, r)was extracted into therequireAuthmiddleware that stashes verifiedClaimsonr.Context()via a typed key; handlers read claims viaclaimsFromCtx. The alternative — keeping auth in handlers and adding rate-limit inside each one — would either double-verify the bearer (one verify for the limit key, one in the handler) or hide the limiter inside each handler, opaque to the route-registration block. Middleware composition keeps the per-route policy visible inRoutes(). - One shared subject bucket across the three /tokens routes (Slice C.4).
subjectRateLimitkeys onhashSubject(claims.Subject)and the same registry covers GET/tokens, DELETE/tokens/:label, and POST/tokens/:label/rotate. Picked over three per-route buckets so a misbehaving client cannot multiply effective throughput by fanning out (3 routes × 10/min would give 30/min effective rather than the operator-intended 10/min). Pinned byTestRateLimitTokensSharedBucketAcrossRoutes. Reserve()+Cancel()on denial, notAllow()(Slice C.4). Functional equivalence — neither pattern consumes budget on denial — butReserve().Delay()gives the precise wait time, which we surface asRetry-Afterwith a 1s minimum floor. (Retry-After: 0reads as "retry immediately" to aggressive clients and would defeat the limiter.) Pinned byTestRateLimitRegistryDenialDoesNotConsumeBudget(50 denials in a tight loop, then 150ms regen window allows the 51st request) and theRetry-Afterassertions on the integration 429 tests.trustForwardedFor: falsedefault in the binary (Slice C.4). The broker behind an Ingress sees the controller's IP inr.RemoteAddr, so the per-IP limiter on/auth/logincollapses into one bucket for every client unless we honor XFF. But trusting XFF when NOT behind a proxy lets an attacker rotate the header to bypass per-IP limits. The binary ships safe; chart-side §6.3.A inverts the default totrue(the chart's "behind an Ingress" assumption holds in deployment).TestRateLimitLoginIPIgnoresForwardedForByDefaultpins the default-untrusted behavior at the binary level;TestRateLimitLoginIPCrossIPIsolationpins the trust-enabled behavior.rateLimit.disablednotenabled(Slice C.4). Same shape assweeper.disabled. Zero-value (false) gives the production-safe behavior so an operator who omits therateLimit:block in their values file still gets the protection. Defaults applied at validate-time: 10/min subject burst 5, 20/min IP burst 5. Field validation skipped entirely whenDisabled: trueso an operator opting out can leave the per-route knobs empty.- Per-replica unbounded registry, by design (Slice C.4). The
rateLimitRegistrykeeps one*rate.Limiterper key with no TTL eviction and no max-entry cap. CodeRabbit flagged this as a memory-DoS vector; rejected after re-examining the threat model. Realistic worst-case sizing is ~500KB at ~10k authenticated subjects — not memory-DoS-shaped. Cluster-shared limiter is the Phase-7+ fix; documented in §Backlog. OIDC_CLIENT_SECRETenv-var override (§6.3.C, additive to §6.2).broker.LoadConfig.applyEnvOverrides()(tools/demarkus-broker/internal/broker/config.go:262) lets the OAuth client secret come from the environment instead of the on-disk config file. Env wins over file when both set; empty env is treated as unset so an accidentally-cleared variable can't blank out a file-supplied value. Production deployments keep the OAuth secret in an externally-managed Kubernetes Secret (External Secrets Operator, Sealed Secrets, Vault) mounted viasecretKeyRef, instead of baking it into the chart-rendered config Secret where it would leak into helm release history. This is a one-line escape hatch for the production-secret-ref deployment shape, NOT a §6.2 reopening —§6.2 broker binary completeframing stands. Pinned byTestLoadConfigOIDCClientSecretEnvOverride(4-row table).- helm-unittest pinned to v0.6.2 in CI (§6.3.D.1). v1.0+ uses
platformHooksin plugin.yaml which needs helm v3.16+; Fritz's local helm is v3.13.2. Pinning v0.6.2 in CI matches the local-dev validation surface so test behavior is identical between developer machines and CI. Bump together when helm itself bumps. Helm pinned to v3.13.2 alongside. test-brokerGo CI job folded into §6.3.D.1. Documented above.- Three-branch hybrid template for upgrade-safe Secrets (§6.3.D.2).
deploy/helm/demarkus-server/templates/tokens.yamlkeys on the live Secret's state: (1) first install → render fresh + keep annotation; (2) legacy upgrade → re-render existing data verbatim + add the annotation; (3) race-free no-op → emit nothing, helm GC blocked by live annotation, broker stays sole writer. Picked over Option A (always render) which keeps a race window betweenhelm template's lookup snapshot andhelm applywhile broker writes viamutateSecret. SIMULATE_LEGACY_NO_KEEPtest-mode flag over a separate script (§6.3.D.2). One script with env-flag exercises both the normal three-branch path and the migration path.test-charts-kindCI job (§6.3.D.2). 47-line job in.github/workflows/ci.ymlrunstest-upgrade-wipe.shthree times: server normal + serverSIMULATE_LEGACY_NO_KEEP+ broker normal.kubectl jsonpathbracket notation for hyphenated keys (§6.3.D.2 CodeRabbit).{.data['key-with-hyphens']}, NOT{.data.key-with-hyphens}. Spec-conformant bracket form. Pin this convention for any future jsonpath against k8s data fields.- Per-service split container images over a unified single-image bundle (§6.7.A). Three images:
ghcr.io/latebit-io/demarkus-server(server + CLI for probes),demarkus-broker(single binary + CA bundle for OIDC),demarkus-agent(single binary + CA bundle for remotemark://crawl TLS). Initial §6.7.A draft packaged all six binaries into oneghcr.io/latebit-io/demarkusimage; reverted to split because demarkus's shape (multi-component product) calls for per-service blast-radius isolation. A CVE in broker's OIDC dependency shouldn't fail the server pod's image scan. - Admin CLIs not bundled in runtime images (§6.7.A).
demarkus-tokenanddemarkus-publishship as standalone binaries viatools/.goreleaser.ymlarchives. Operators run them locally with port-forward or in dedicated Job pods, NOT viakubectl execinto long-running pods. Bundling admin tools into runtime images would leak "admin tooling" into the pod's blast radius. - CA bundle selectively included (§6.7.A). Broker + agent images copy
/etc/ssl/certs/ca-certificates.crtfrom the build stage. Server image skips it — no outbound TLS today; bundled CLI's exec probes use-insecurefor localhost. USER 65532:65532in all three runtime stages (§6.7.A). Matches each chart'spodSecurityContext.runAsUserfor defense-in-depth.go build -C <module>overRUN cd <module> && go build(§6.7.A). Go 1.20+ idiom; auto-creates-oparent directory.- No SHA256 digest pinning on
golang:1.26-alpine(§6.7.A CodeRabbit nit skipped). Floating the minor tag picks up Go stdlib security patches automatically; pinning trades reproducibility for a maintenance hazard without renovate automation. Revisit when base-image automation lands. docker/build-push-action@v6over re-introducing goreleaser'sdockers:block (§6.7.B.1 PR #122). The new multi-stage Dockerfiles compile from source via Go's cross-compilation under buildx, not by COPY-ing goreleaser-built binaries. Wiring image release through build-push-action keeps the Dockerfiles unchanged and portable (anyone candocker buildlocally without goreleaser-pro). Cost: binaries compile twice on release (once for archives, once inside image build) — Go is fast, ~30s extra per platform, negligible.- Chart-version + appVersion pinned 1:1 to module version on release (§6.7.B.3 PR #124). Coupled lifecycle: chart-of-version-X always references image-of-version-X. Independent chart versioning would be useful for chart-only fixes (broken template, NOTES.txt edit), but adds tag-scheme complexity for a use case without evidence yet. Revisit if it materializes.
- OCI helm auth via shared
~/.docker/config.json(§6.7.B.3). Helm 3.13+ reads the docker config natively for OCI registry auth; the existingdocker/login-actionstep satisfies both image push and chart push. No separatehelm registry loginstep needed. tag_prefix: tools/v+change_path: tools/(§6.7.B.2 semver-tools job). PaulHatch/semantic-version mirror of server/client semver jobs; tools/ is now a peer module in the release-pipeline taxonomy with independent version bumps.
Open Questions
- First customer trial. Nesto (
*.library.nesto.ca) is path-B — trial waits for product. Trial runbook lands at/trials/nesto.mdwhen scoping starts. - Two-week deadline (2026-05-27): §6.7 fully landed; §6.5 / §6.6 / §6.4 remain. ~11 days runway. Suggested order: §6.5 → §6.6 → §6.4 (per Fritz's earlier confirmation). §6.5 is independent and parallelizable; §6.6 grows incrementally; §6.4 depends on §6.7.B published artifacts (now available).
- First module-release tag firing of the §6.7.B pipeline. Triggered automatically by the next push to main that touches
server/,client/, ortools/. Until a first push fires, no image or chart is yet on GHCR despite the pipeline being live. Operators building from main still needmake imagelocally or--set image.repository=...overrides. Resolves on the firstfeat:orfix:commit to a module path. - Hardened broker chart symmetry (deferred from §6.3.D.2). The broker chart's
secret-issuances.yamluses the simpler lookup-skip pattern. Defense-in-depth follow-up; not load-bearing. Only act on if Fritz brings it up.
Repository Layout
Reflects state as of §6.7.B.3 merge (2026-05-14):
deploy/
helm/
demarkus-server/ # one-world chart (Phase 6.1) — shipped PR #107
demarkus-broker/ # OIDC token broker chart (Phase 6.3)
demarkus-agent/ # crawl/index agent chart (Phase 6.0) — shipped PR #106
test-upgrade-wipe.sh # kind regression: §6.3.D.2 PR #119
k8s/
examples/
applicationset.yaml # Argo CD ApplicationSet (§6.4 — pending)
kustomize-overlay/ # Kustomize alternative (§6.4 — pending)
observability/ # per-backend log-shipper configs (§6.5 — pending)
docs/
deployment/ # install / security / operations / observability
# (§6.6 — pending; location confirmed 2026-05-13)
.dockerignore # repo-root, applies to all three image builds
# (§6.7.A PR #121)
.github/
workflows/
release.yml # full per-module release pipeline:
# - server/v* → goreleaser archives
# + docker buildx push demarkus-server image
# + helm package + push OCI chart
# - client/v* → goreleaser archives
# + docker buildx push demarkus-agent image
# + helm package + push OCI chart
# - tools/v* → goreleaser archives (broker+token+publish)
# + docker buildx push demarkus-broker image
# + helm package + push OCI chart
# (§6.7.B PRs #122 + #123 + #124)
ci.yml # PR + push tests; test-broker + test-charts +
# test-charts-kind (§6.3.D.1, §6.3.D.2)
protocol/
auth.go # HashToken — sha256-<hex> contract (Slice A)
token/ # Generate, ReadFile, AppendEntry, WriteFile,
# FormatEntry, flock helpers (Slice A) +
# AppendBytes, RemoveBytes in-memory (Slice B) +
# ParseBytes (Slice C.2).
store/ # versioned content-store primitives, hoisted
# from server/internal/store (§6.7.0 PR #120).
server/
Dockerfile # multi-stage; bundles demarkus-server + CLI
# (§6.7.A PR #121)
.goreleaser.yml # binary archives only
cmd/
demarkus-server/
client/
.goreleaser.yml # binary archives for demarkus, demarkus-tui,
# demarkus-mcp (demarkus-agent ships via Dockerfile
# build in release-client)
cmd/
demarkus/
demarkus-tui/
demarkus-mcp/
demarkus-agent/
Dockerfile # (§6.7.A PR #121)
tools/
.goreleaser.yml # binary archives for demarkus-broker,
# demarkus-token, demarkus-publish (§6.7.B PR #123)
demarkus-broker/
Dockerfile # (§6.7.A PR #121)
demarkus-token/ # admin CLI (no Dockerfile — archive-only)
demarkus-publish/ # admin CLI (no Dockerfile — archive-only)
Sub-Phases
6.0 — demarkus-agent verified + chart ✓ (merged PR #106)
Existing demarkus-agent binary verified end-to-end (2 team worlds + 1 hub smoke test) and chart at deploy/helm/demarkus-agent/ shipped. Stateless Deployment; ConfigMap holds TOML agent config; Secret holds per-host tokens; exec liveness probe; outbound-only (no Service). §6.7.A PR #121 fixed a pre-existing structural bug in the agent deployment test suite (10 silently-erroring tests, same shape as the server-statefulset suite). Agent ships per-image (ghcr.io/latebit-io/demarkus-agent) with CA bundle for outbound TLS, released via client/v* tag flow (§6.7.B PR #122 + #124).
6.1 — demarkus-server Helm chart ✓ (merged PR #107) + §6.3.D.2 ✓ + §6.7.A image fix ✓ + §6.7.B release ✓
deploy/helm/demarkus-server/ — StatefulSet, 1 replica, volumeClaimTemplates, exec probes against /.well-known/agent-manifest.md, Service type LoadBalancer protocol: UDP. Bootstrap path seeds the initial admin token via protocol/token.Generate + AppendEntry. Cert-manager Certificate resource behind a flag. helm-unittest test files at tests/ wired into CI by §6.3.D.1 PR #118 and validated end-to-end against a kind cluster by §6.3.D.2 PR #119.
§6.3.D.2 sidecar: tokens Secret now carries helm.sh/resource-policy: keep via three-branch hybrid template. PR #119 merged 2026-05-13. test-charts-kind CI job pins the upgrade-wipe property end-to-end.
§6.7.A image fix: chart's image.repository (default ghcr.io/latebit-io/demarkus-server) is built from server/Dockerfile as multi-binary image (demarkus-server + demarkus CLI). PR #121. The §6.1 probe caveat is empirically closed (kind smoke verified Ready in 19s).
§6.7.B release plumbing: next push to main touching server/ triggers server/v* tag → goreleaser archives + multi-arch image push to ghcr.io/latebit-io/demarkus-server:<version> + OCI chart push to oci://ghcr.io/latebit-io/charts/demarkus-server:<version>. PRs #122 + #124.
6.2 — demarkus-broker binary (tools/demarkus-broker/) ✓ complete
Slice B (single-world OIDC mint flow) merged as PR #109 (commit 2a6aae6, 2026-05-11). Slice C.1 PR #110. Slice C.2 PR #111. Slice C.3 PR #112. Slice C.4 PR #114. §6.2 broker binary complete.
The §6.3.C OIDC_CLIENT_SECRET env-var override is a one-line additive surface that does NOT reopen §6.2.
6.3 — demarkus-broker Helm chart ✓ complete (all five sub-slices merged) + §6.7.B release ✓
Multi-replica HA at deploy/helm/demarkus-broker/, shipped across five sub-PRs (#115, #116, #117, #118, #119), all merged 2026-05-13. Chart now released via tools/v* tag flow (§6.7.B PR #123 + #124): image push to ghcr.io/latebit-io/demarkus-broker:<version> + OCI chart push to oci://ghcr.io/latebit-io/charts/demarkus-broker:<version>.
6.4 — Universe topology examples (pending)
ApplicationSet + Kustomize overlay at deploy/k8s/examples/. Both reference the chart pull URLs from §6.7.B (now live). Argo CD ApplicationSet template generator iterates a worlds: list and templates one Application per world. Kustomize overlay is the GitOps-without-Argo alternative. Customer's ops team picks one. Verification surface: argocd app create --dry-run or kustomize build against a kind cluster.
6.5 — Observability recipes (pending — independent of any prior phase, can start immediately)
Per-backend configs in deploy/observability/. Slice C.2 added sweeper-side log lines (broker: swept, broker: sweep failed, broker: sweeper observing new leader, broker: sweeper lost leadership); Slice C.3 added rotate-side log lines; Slice C.4 added rate-limit log lines (broker: rate limit exceeded with route, subject or ip, retryAfter fields). Recipes are pure config — no core code changes. Datadog autodiscovery via pod annotations + a dashboard JSON; OTel Collector via a config.yaml; Vector via a [transforms.parse] block; Fluent Bit via parser + filter; Grafana Alloy via discovery.kubernetes + loki.process.
6.6 — Documentation suite (pending — incremental)
docs/deployment/*.md (location confirmed 2026-05-13). Per-chart READMEs at deploy/helm/<chart>/README.md (broker README exists from §6.3.C; server + agent need similar treatment). Install guide (per-provider OIDC setup for Google / Okta / Entra ID / Auth0; chart install order: server → broker → agent; cert-manager + DNS topology). Security/threat model. Operations (upgrade path including the now-tested resource-policy:keep contract, backup/DR via Velero or demarkus-agent sync). Observability recipes (links to §6.5).
6.7 — Release pipeline ✓ complete
§6.7.0 ✓ merged (PR #120, commit b210015) — Precursor refactor. Hoisted server/internal/store/ → protocol/store/. Pure namespace move; four import-path rewrites.
§6.7.A ✓ merged (PR #121, commit aa9a3c6) — Per-service split container images + CLI relocation. Three Dockerfiles co-located with their primary binaries. Server image bundles CLI for probes; broker + agent images include CA bundle for outbound TLS. All three runtime stages USER 65532. Admin CLIs (token, publish) ship as standalone binaries — not bundled in any runtime image.
§6.7.B ✓ complete via three sub-PRs:
- §6.7.B.1 ✓ merged (PR #122, commit
0e1a4ae, 2026-05-13) — addeddocker/build-push-action@v6to existingrelease-server+release-clientjobs. Multi-arch (amd64/arm64/armv7) push toghcr.io/latebit-io/{demarkus-server,demarkus-agent}:<version>onserver/v*/client/v*tag fire. - §6.7.B.2 ✓ merged (PR #123, commit
afb6155, 2026-05-13) — newtools/.goreleaser.ymlwith three archive builds (broker + token + publish). New workflow plumbing:toolsfilter,test-tools,semver-tools,release-tools.release-toolscreatestools/v*tag, runs goreleaser, builds + pushes broker image toghcr.io/latebit-io/demarkus-broker:<version>. - §6.7.B.3 ✓ merged (PR #124, commit
9cf6303, 2026-05-14) —azure/setup-helm@v4+helm package+helm pushat the end of each of the three release jobs. Chartversion+appVersionpinned 1:1 to module version. Charts push tooci://ghcr.io/latebit-io/charts/{demarkus-server,demarkus-broker,demarkus-agent}. Helm 3.13+ uses the docker login config natively for OCI auth.
§6.7 fully complete. All three runtime images + all three OCI helm charts have a release path tied to their corresponding module-version tag.
Sequencing
- Slice A — token-mint library ✓ merged 2026-05-11 (PR #108).
- 6.0 chart ✓ merged 2026-05-11 (PR #106).
- 6.1 server chart ✓ merged 2026-05-11 (PR #107). §6.3.D.2 ✓. §6.7.A image fix ✓. §6.7.B release ✓.
- 6.2 broker binary — Slices B + C.1–C.4 ✓ all merged 2026-05-11–12 (PRs #109, #110, #111, #112, #114).
- 6.3 broker chart ✓ all five sub-slices merged 2026-05-13 (PRs #115, #116, #117, #118, #119).
- 6.7.0 store hoist ✓ merged 2026-05-13 (PR #120,
b210015). - 6.7.A image consolidation + CLI relocation ✓ merged 2026-05-13 (PR #121,
aa9a3c6). - 6.7.B release pipeline ✓ three sub-PRs merged 2026-05-13–14:
- 6.7.B.1 (PR #122,
0e1a4ae) — image release on existing server/v* + client/v* tags. - 6.7.B.2 (PR #123,
afb6155) — tools/v* tag flow + broker image release. - 6.7.B.3 (PR #124,
9cf6303) — OCI chart publish for all three charts.
- 6.7.B.1 (PR #122,
- 6.5 observability recipes — NEXT. Independent of any prior phase. Pure config files.
- 6.6 docs — incremental throughout 6.5/6.4 plus dedicated polish pass.
- 6.4 topology examples — last; references chart pull URLs from §6.7.B (now live).
Rough effort: ~10 days of focused work remaining for §6.5 + §6.6 + §6.4 against the 2026-05-27 deadline. Phase 6 ~85% complete by sub-phase count (6.0/6.1/6.2/6.3/6.7 done; 6.4/6.5/6.6 remaining).
Backlog (deferred, easy to add later)
- Cosign signing of images + chart releases. Half-day CI add.
- Latency log-enrichment (
duration_msfield on request slog lines). Tiny additive change. - SCIM lifecycle webhook on the broker. Optional add when a customer asks.
- Userinfo-based groups in the broker
Verifier. Slice C.1 ships ID-token-only;AllowEmailsis the documented workaround. - Case-sensitive group matching for Keycloak. Slice C.1 chose case-insensitive for the validated IdP set.
- Configurable lease timings (
sweeper.leaseDuration,renewDeadline,retryPeriod). C.2 hardcodes the client-go defaults. - SIGHUP on revoke + sweep + rotate to shrink the revoke-to-effect window. Needs
pods/execRBAC across world namespaces. - Sweeper backing-store migration when the issuances Secret hits the ~1MB / ~5000-issuance k8s storage ceiling.
- Cluster-shared rate limiter (Slice C.4 follow-up).
- Idle-key GC for the rate-limit registry (Slice C.4 follow-up).
- Timeouts on the existing
test-protocol/test-server/test-clientCI jobs. §6.3.D.1 addedtimeout-minutes: 20to the new jobs; the older jobs still rely on the GitHub Actions default (360 min). One-line cleanup. - Hardened broker chart Secret symmetry. Broker's
secret-issuances.yamluses the simpler lookup-skip pattern (vs. the §6.3.D.2 three-branch hybrid on the server chart). Defense-in-depth follow-up; not load-bearing. - Base-image digest pinning (§6.7.A CodeRabbit nit skipped). When renovate/dependabot is wired to bump base-image digests, pin all three Dockerfiles to specific SHAs for reproducibility.
- Single-image distribution mode (§6.7.A architectural alternative). If a customer asks for one-image-with-all-binaries, the unified-Dockerfile shape from the §6.7.A draft can be added as an opt-in. The split-images shape stays the default.
- Server image CA bundle (§6.7.A deferred). If a need arises for
kubectl execdebugging or future outbound-TLS server features. One-line add. - Version-stamping for
demarkus-server+demarkusCLI +demarkus-token+demarkus-publish(§6.7.B deferred). Currentlyvar versionis wired indemarkus-agentanddemarkus-brokeronly; goreleaser's-Xflag on the others is a no-op. Addvar version = "dev"+ a-versionflag to each missing binary; surface via--versionsubcommand. Out of §6.7 scope; small follow-up PR. - Independent chart versioning (§6.7.B.3 deferred). Currently chart-version is pinned 1:1 to module version on release. If we need to ship a chart-only fix (broken template, NOTES.txt edit) without bumping the image, introduce a
charts/<name>/v*tag scheme. Adds tag-scheme complexity; revisit when we have evidence of need.
Risks
- Observability via logs ceiling. If customers want signals not derivable from current slog, we hit a wall. Mitigation: log enrichment is a small additive change.
- Broker secret-write blast radius. Mitigated by namespace-scoped
Roles, audit log, optionalNetworkPolicy. Per-worldRoleis non-negotiable.TestMintRBACDeniedNoPartialStatevalidates clean failure. - OIDC provider coupling. First impl is Google; structure so second provider is a one-day add.
- Token revocation in-flight latency. Revocation latency = kubelet Secret propagation + whatever the server does on re-mount. Acceptable for the deployment posture (~60s default kubelet propagation).
- Rotate transient two-tokens window. Documented; verified by
TestRotateTokenSoftPartial. - §6.1 chart upgrade-wipe gap. ✓ Resolved by §6.3.D.2 PR #119.
- §6.1 chart-test plumbing latent failure. ✓ Resolved by §6.3.D.1 PR #118; agent chart equivalent resolved by §6.7.A PR #121.
- §6.7 multi-binary image hand-off. ✓ Resolved by §6.7.A PR #121.
- Release-pipeline image gap. ✓ Resolved by §6.7.B PRs #122 + #123 + #124. First module-touching commit to main will fire the corresponding release flow and produce a fully-aligned chart+image bundle.
- Issuances Secret scaling wall at the ~1MB / ~5000-issuance k8s storage ceiling. Phase-7+ fix is a different backing store.
- Chart proliferation. Three charts + examples + dashboards. Mitigate with shared common-labels templates.
- Trial scope creep. First customer is path-B.
- Per-replica rate limit ⇒ N× effective rate. Slice C.4 documented; deferred fix in §Backlog.
- Rate-limit registry growth under misconfigured XFF trust. Documented in C.4 §Decisions and chart README.
- First module-release tag fire is in-cycle on a
feat:orfix:commit to a module path. Until that fires, no image or chart is on GHCR despite the pipeline being live. Operators building from main needmake imagelocally in the interim. Not a code risk; an operational one. Closes naturally on the first qualifying merge to main.
Status
Plan v17, 2026-05-14. Slice A, 6.0 chart, 6.1 chart (incl. §6.3.D.2 + §6.7.A image fix + §6.7.B release), 6.2 broker binary (Slices B + C.1–C.4), 6.3 broker chart (all five sub-slices), 6.7.0 store hoist, 6.7.A image consolidation + CLI relocation, 6.7.B release pipeline all merged (PRs #106, #107, #108, #109, #110, #111, #112, #114, #115, #116, #117, #118, #119, #120, #121, #122, #123, #124).
14 PRs merged across 2026-05-13–14 (the §6.3 → §6.7 sprint): #115 (6.3.A) → #116 (6.3.B) → #117 (6.3.C) → #118 (6.3.D.1) → #119 (6.3.D.2) → #120 (6.7.0 store hoist) → #121 (6.7.A image consolidation) → #122 (6.7.B.1 image release) → #123 (6.7.B.2 tools release) → #124 (6.7.B.3 OCI chart publish). Phase 6 ~85% complete by sub-phase: §6.0 / §6.1 / §6.2 / §6.3 / §6.7 done; §6.4 / §6.5 / §6.6 remaining.
§6.7.B trajectory recap (three sub-PRs, same-day):
- 6.7.B.1 (PR #122,
0e1a4ae) —docker/build-push-action@v6added to existing server/v* + client/v* release jobs. Multi-arch push to demarkus-server + demarkus-agent images. Chose build-push-action over re-introducing goreleaser'sdockers:block to keep multi-stage Dockerfiles unchanged and portable. - 6.7.B.2 (PR #123,
afb6155) — newtools/.goreleaser.yml(broker + token + publish archives) + newtoolsfilter/test-tools/semver-tools/release-tools jobs in workflow. tools is now a peer module with independenttools/v*tag-driven release cadence. Admin CLIs (token, publish) ship as archives only — no runtime image per §6.7.A decision on minimal pod blast radius. - 6.7.B.3 (PR #124,
9cf6303) — OCI chart publish viahelm package --version $VER --app-version $VER+helm push oci://ghcr.io/latebit-io/charts/<chart>. Chart-version + appVersion pinned 1:1 to module version on release. Helm 3.13+ uses the docker login config natively for OCI auth, so no separate registry login step.
Next: §6.5 observability recipes. Independent of any prior phase. Per-backend log-shipper configs in deploy/observability/{datadog,otel-collector,vector,fluent-bit,grafana-alloy}/ mapping demarkus's existing structured slog output to each backend. Pure config files; no Go changes. Customer's SRE picks one. Parallelizable with §6.6 docs work. Then §6.4 topology examples last (now unblocked by the §6.7.B chart OCI publish — examples can reference oci://ghcr.io/latebit-io/charts/<chart> pull URLs). Two-week deadline at 2026-05-27 with ~13 days remaining.