---
id: w1/m14/t003
title: Optional public route (Traefik TCP/SNI, mirroring `BEX_DB_DOMAIN`)
worker: worker1
status: done
estimate: 30m
depends_on: [w1/m14/t002]
---

## Objective

Add optional public access to a `KeyValue` instance: when a DB-domain-style env (mirroring `BEX_DB_DOMAIN`) is set and the CR opts in via `spec.public`, the reconciler creates a Traefik TCP router with SNI routing `<id>.<kv-domain>` → the Valkey Service on a shared entrypoint. Also settle the connection-info Secret shape (host/port/password/URL forms) that a future API layer will read to assemble the connection panel.

## Context

- `docs/ADR009-postgresql-management.md` §3 — the external-path pattern to mirror: a Traefik TCP router with SNI on one wildcard entrypoint (`*.db.bex.co`) fans out to each backend Service; created only when `spec.public: true` **and** the domain env is set (private by default). Reuse the same mechanism for Valkey (`:6379`).
- `BEX_DB_DOMAIN` (operator env, per root `CLAUDE.md`) — the Postgres analog; introduce a mirroring `BEX_KV_DOMAIN` (or equivalent) so `<id>.<kv-domain>` hostnames route via Traefik TCP/SNI; unset ⇒ internal-only.
- The route is opt-in per instance via `spec.public` (added in t001) and immutable-ID-in-SNI so a rename never breaks a connection string (same reasoning as the Postgres SNI hostname).
- Connection-info Secret: extend the Secret from t002 (or a companion) with host/port/password and assembled internal + external URL forms (`redis://` / `rediss://`), so the later REST/GraphQL/MCP layer can surface a Connections panel without re-deriving them.
- Note the same TCP-not-HTTP caveat: the public wildcard must be DNS-only (gray-cloud), not Cloudflare-proxied — Cloudflare's proxy is HTTP(S) only.

## Steps

1. Add a `BEX_KV_DOMAIN`-style operator env (mirror `BEX_DB_DOMAIN`) read by the reconciler; unset ⇒ skip the public route.
2. When `spec.public` is true and the domain env is set, create/update a Traefik TCP router (IngressRouteTCP or equivalent) with SNI `<id>.<kv-domain>` → the Valkey Service on the shared TCP entrypoint; owner-referenced so delete drops the route.
3. Populate the connection-info Secret with host/port/password + internal and external URL forms.
4. Regenerate manifests/RBAC as needed (`make manifests generate` from `lego/operator/`) and document the new env var in the root `CLAUDE.md` env table.

## Files

- `lego/operator/internal/controller/keyvalue_controller.go` (extend from t002)
- root `CLAUDE.md` — add the `BEX_KV_DOMAIN`-style env row to the environment-variables table
- `lego/operator/config/rbac/` (if new resource permissions are needed)

## Acceptance criteria

- [ ] With the domain env set and `spec.public: true`, the reconciler creates a Traefik TCP/SNI route `<id>.<kv-domain>` → the Valkey Service; with either unset, no public route is created (internal-only).
- [ ] The connection-info Secret carries host/port/password + internal and external URL forms.
- [ ] Deleting the `KeyValue` removes the public route (owner-referenced); the new env var is documented in `CLAUDE.md`.

## Out of scope

- envtest coverage (t005) and the `/simplify` pass (t004).
- Broad-client SNI-proxy compatibility beyond direct-TLS (same deferral the Postgres ADR makes).
- Any REST/GraphQL/MCP/dashboard surface that reads the connection-info Secret (deferred to w2/w5).
