ADR 0001: per-environment namespaces and header-based routing

Date: 2026-09-09. Status: accepted.

Context

An Environment redeploys only the services a developer changed and shares the rest with the Baseline. The copies must be isolated for quota and cleanup, yet still reach shared services by their normal in-cluster names, and traffic for one developer must reach that developer's copies without touching anyone else's.

Decision

  1. One namespace per Environment, named <workspace>-<application>-<environment>. The operator labels it, applies a ResourceQuota from the matching Policy, and deletes the whole namespace when the Environment is deleted. Nothing else needs cleaning up.
  2. Shared services are ExternalName aliases in the environment namespace pointing at the baseline instance in the application namespace. Copies keep calling ledger and nothing in the application has to change.
  3. Routing is by HTTP header X-Nazeel-Env: <environment>. A Go reverse proxy owned by Nazeel will sit behind every baseline Service name and send requests carrying the header to <service>.<environment-namespace>, everything else to the baseline. The preview host <environment>--<application>.<domain> sets the header at the edge. Applications must forward the header hop to hop, like trace context; W3C baggage: nazeel-env= is accepted as an alternative carrier. Raw TCP dependencies are never copied.
  4. Shared dependencies cannot be overridden. Databases and queues stay on the baseline.
  5. API group is nazeel.sa, version v1alpha1, module github.com/nazeel/nazeel/operator.

Consequences

Addendum 2026-09-09: router implementation decisions

Approved with the router binary (operator/cmd/router). All of these are enforced in code.

Addendum 2026-09-09: rendering services from git

The Baseline's spec.source is rendered by operator/internal/source into the same normalized service list that inline spec.services provides. Decisions:

Addendum 2026-09-09: TTL, idle sleep and wake

Addendum 2026-09-09: admission webhook and webhook certificates

Addendum 2026-09-10: narrowing the operator's Secret access

The rendering addendum recorded cluster-wide Secret read as an accepted v0 compromise. It is now narrowed: the operator reads only Secrets labelled nazeel.sa/git-credentials=true, and reports SourceResolved=False/ResolveFailed with the exact instruction when a referenced Secret lacks the label. Connecting an application therefore never grants the operator sight of unrelated application secrets, which is what a buyer's security team asks about first. The chart's ClusterRole keeps the get/list/watch verbs because label-based restriction is enforced by the admission of the request, not by RBAC alone; sites that need RBAC-level enforcement can replace the ClusterRole with per-namespace Roles listing the Secret names.

Addendum 2026-09-12: terminal session limits and recording retention

The terminal was specified as "every session is recorded and audited". That was right and is unchanged, but it left two things unsaid, and both turned out to matter more than the recording itself.

Nothing closed a session. A shell left open at a prompt overnight is the classic privileged-access finding, and the product had no answer to it. Sessions are now bounded by the team's Policy: terminalIdleTimeout (15m by default) and terminalMaxDuration (4h). Either may be set to "0" to disable it, which has to be an explicit decision rather than the state of a fresh install. Why the reason is recorded: "the shell exited" and "we closed it because it sat idle for an hour" are different facts to whoever reads the log afterwards, and only the second answers the review question.

Nothing removed a recording. Recordings share the volume with the SQLite database, so they were the one thing on the install that grew without bound, and filling that volume makes the database read-only — which stops the audit log, the one thing this product promises will always be there. terminalRecordingRetentionDays (90 by default) deletes recordings older than that; 0 keeps them forever, for sites whose regulator asks for it, and the volume gauge and dashboard banner exist so that choice is made with the consequence visible.

Retention deletes the recording, never the audit entry. The entry saying a session happened, who opened it and why it ended is permanent; only the replayable file ages out, and its expiry is itself audited as actor system. A retention policy that could remove audit entries would be a way of erasing history rather than of managing disk, and the append-only triggers on the audit table would refuse it anyway.

A recording is capped at 20 MiB, and reaching the cap ends the session. The alternative — carrying on with the recording stopped — would leave an unrecorded shell running, which is the one state the terminal must never be in. A runaway process reaches the cap in seconds, which is the case it exists for; a person typing will not reach it in a working day.