diff --git a/README.md b/README.md
index b129ede..e92d74c 100644
--- a/README.md
+++ b/README.md
@@ -1,138 +1,101 @@
-## datum dns-operator
-
-Kubernetes operator for managing DNS zones and records, with a pluggable backend architecture. This repository provides:
-- Custom resources to model zones, recordsets, and zone classes
-- Controllers for two runtime roles:
- - "downstream" agent that programs a DNS backend (PowerDNS supported)
- - "replicator" that mirrors resources from an upstream cluster to a downstream cluster and synthesizes status
-- Kustomize overlays to deploy either role
-
-### CRDs
-- **`DNSZoneClass`** (cluster-scoped)
- - `spec.controllerName`: selects backend controller (e.g., "powerdns")
- - `spec.nameServerPolicy`: currently supports `Static` with `servers: []`
- - `spec.defaults.defaultTTL`: optional default TTL for zones
-
-- **`DNSZone`** (namespaced)
- - `spec.domainName`: required zone FQDN (e.g., `example.com`)
- - `spec.dnsZoneClassName`: optional reference to a `DNSZoneClass`
- - `status.nameservers`: authoritative nameservers (derived from class policy)
- - `status.conditions`: `Accepted`, `Programmed`
-
-- **`DNSRecordSet`** (namespaced)
- - `spec.dnsZoneRef`: `LocalObjectReference` to a `DNSZone` in the same namespace
- - `spec.recordType`: one of `A, AAAA, CNAME, TXT, MX, SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB`
- - `spec.records[]`: owners with typed fields per record type (or `raw` strings). TTL per-owner optional.
- - `status.conditions`: `Accepted`, `Programmed`
-
-### Controllers and Roles
-
-- **Downstream role** (`--role=downstream`)
- - `DNSZoneReconciler`: when `DNSZone.spec.dnsZoneClassName` references a class with `controllerName: powerdns`, ensures the zone exists in PowerDNS and honors static nameserver policy.
- - `DNSRecordSetReconciler`: for PowerDNS-backed zones, applies recordsets to PDNS using an authoritative mode that REPLACEs desired owners and DELETEs extraneous owners of the same type. Requeues while the zone is not ready.
-
-- **Replicator role** (`--role=replicator`)
- - Multicluster manager discovers one or many upstream clusters (single-cluster or Milo discovery) and mirrors `DNSZone`/`DNSRecordSet` into a configured downstream cluster using a mapped-namespace strategy.
- - `DNSZoneReplicator`:
- - Mirrors upstream `spec` into a downstream shadow object
- - Ensures an operator-managed upstream `DNSRecordSet` named `soa` exists (typed SOA targeting `@`) for PowerDNS-backed zones
- - Updates upstream `status`: sets `Accepted=True` and currently treats `Programmed=True` optimistically; fills `status.nameservers` from `DNSZoneClass` when `Static` policy is set
- - `DNSRecordSetReplicator`:
- - Mirrors upstream `spec` into a downstream shadow object
- - Updates upstream `status`: `Accepted` reflects `DNSZone` presence; `Programmed=True` once downstream shadow ensured
-
-### Backends
-- **PowerDNS (Authoritative)**
- - Enabled when `DNSZoneClass.spec.controllerName: powerdns`
- - The downstream agent uses environment variables to connect:
- - `PDNS_API_URL` (default `http://127.0.0.1:8081`)
- - `PDNS_API_KEY` or `PDNS_API_KEY_FILE`
- - Recordset translation supports typed fields for all declared RR types and sensible normalization of names and quoting for TXT/targets.
-
-### Deployment Overlays
-
-- `config/agent/`
- - Namespace: `dns-agent-system`
- - Runs the operator with `--role=downstream`
- - Merges a `pdns` sidecar container into the controller Deployment to run PowerDNS alongside the manager
- - Mounts a shared `emptyDir` to exchange an auto-generated API key, and sets `PDNS_API_KEY_FILE` in the manager
- - Provides a `Service` exposing PDNS ports 53/udp, 53/tcp, and 8081/tcp
- - ConfigMap `server-config` wired to `--server-config`
-
-- `config/overlays/replicator/`
- - Namespace: `dns-replicator-system`
- - Runs the operator with `--role=replicator`
- - Requires a Secret `downstream-kubeconfig` containing key `kubeconfig` to target the downstream cluster
- - ConfigMap `server-config` sets discovery mode (defaults to `single`) and points `downstreamResourceManagement.kubeconfigPath` to `/downstream/kubeconfig`
-
-### Quickstart: Agent with embedded PowerDNS
-1. Install CRDs and default manifests:
- - `kubectl apply -k config/agent`
-2. Create a `DNSZoneClass` for PowerDNS with static nameservers, for example:
-```yaml
-apiVersion: dns.networking.miloapis.com/v1alpha1
-kind: DNSZoneClass
-metadata:
- name: powerdns
-spec:
- controllerName: powerdns
- nameServerPolicy:
- mode: Static
- static:
- servers: ["ns1.example.net.", "ns2.example.net."]
-```
-3. Create a `DNSZone` and a `DNSRecordSet`:
-```yaml
-apiVersion: dns.networking.miloapis.com/v1alpha1
-kind: DNSZone
-metadata:
- name: example-com
- namespace: default
-spec:
- domainName: example.com
- dnsZoneClassName: powerdns
----
-apiVersion: dns.networking.miloapis.com/v1alpha1
-kind: DNSRecordSet
-metadata:
- name: www-a
- namespace: default
-spec:
- dnsZoneRef:
- name: example-com
- recordType: A
- records:
- - name: www
- a:
- content: ["192.0.2.10", "192.0.2.11"]
- ttl: 300
+# DNS
+
+Manage authoritative DNS the Kubernetes way. Declare a domain and its records as
+ordinary Kubernetes resources, and the DNS service programs them into an
+authoritative DNS backend and publishes them to a globally distributed serving
+layer — no zone files, no backend API calls, no manual nameserver wiring.
+
+You manage DNS through native Kubernetes resources, so it works with `kubectl`
+and any Kubernetes client, and inherits your platform's identity, RBAC, and audit
+controls.
+
+This repository provides the **DNS operator**, the control-plane component that
+reconciles those resources and programs the backend. See the
+[Architecture Overview](docs/architecture/README.md) for how the operator fits
+into the wider service.
+
+## What it does
+
+- **Zones and records as resources** — Model domains with `DNSZone` and records
+ with `DNSRecordSet`, covering `A`, `AAAA`, `CNAME`, `ALIAS`, `TXT`, `MX`,
+ `SRV`, `CAA`, `NS`, `SOA`, `PTR`, `TLSA`, `HTTPS`, and `SVCB` types.
+- **Pluggable backends** — A cluster-scoped `DNSZoneClass` selects the backend
+ and nameserver policy, keeping backend choice out of individual zones.
+ [PowerDNS](https://doc.powerdns.com/authoritative/) is supported today.
+- **Multi-tenant by design** — Each tenant authors DNS in their own control
+ plane; the operator discovers and serves many control planes from one shared
+ authoritative backend, with per-domain ownership accounting.
+- **Automatic zone bootstrap** — Default `SOA` and `NS` records are created for
+ every zone from its nameserver policy, without clobbering user-authored apex
+ records.
+- **Clear status** — `Accepted` and `Programmed` conditions report whether a
+ zone or record is valid and actually serving, mirrored back from the
+ authoritative backend.
+
+## How it works
+
+Users declare `DNSZone` and `DNSRecordSet` resources in their own control plane.
+A **replicator** mirrors that desired state into a shared authoritative cluster,
+where a **downstream agent** programs it into the DNS backend. The authoritative
+data is then replicated to a read-only serving layer that answers live queries.
+
+For the full picture — components, control planes, and the serving layer — see
+the [Architecture Overview](docs/architecture/README.md).
+
+## Documentation
+
+**Architecture**
+- [Architecture Overview](docs/architecture/README.md) — System design and core
+ concepts
+- [Deployment Topology](docs/architecture/topology.md) — Roles, control planes,
+ and the serving layer
+- [Replication Model](docs/architecture/replication.md) — How desired state and
+ status move between clusters
+- [API Reference](docs/architecture/api-reference.md) — Full resource schema and
+ conditions
+
+**Guides**
+- [Service Catalog](config/components/service-catalog/README.md) — DNS as a
+ billable platform service
+
+## Deploying
+
+The operator runs in one of two roles, deployed with the Kustomize overlays in
+[`config/`](config). See [Deployment Topology](docs/architecture/topology.md) for
+how the roles fit together.
+
+### Agent with embedded PowerDNS
+
+Runs the operator as a downstream agent alongside PowerDNS and a storage backend
+— the quickest way to a working DNS service:
+
+```sh
+kubectl apply -k config/overlays/agent-powerdns
```
-### Quickstart: Replicator (upstream → downstream)
-1. Create Secret on the replicator namespace containing the downstream kubeconfig (`data.kubeconfig`):
-```bash
+Then create a `DNSZoneClass`, `DNSZone`, and `DNSRecordSet` (see
+[`config/samples`](config/samples) and the
+[API Reference](docs/architecture/api-reference.md)).
+
+### Replicator (upstream → downstream)
+
+Runs the operator as a replicator that mirrors DNS resources from tenant control
+planes into a downstream authoritative cluster:
+
+```sh
+# Provide the downstream cluster kubeconfig
kubectl -n dns-replicator-system create secret generic downstream-kubeconfig \
--from-file=kubeconfig=/path/to/downstream/kubeconfig
-```
-2. Deploy replicator overlay:
-```bash
+
kubectl apply -k config/overlays/replicator
```
-3. Create `DNSZoneClass` (cluster-scoped), `DNSZone` and `DNSRecordSet` on the upstream cluster. The replicator will mirror them into the downstream cluster and update upstream `status` conditions.
-
-### Conditions
-- `Accepted`: resource is valid and has required dependencies (e.g., `DNSRecordSet` sees its `DNSZone`)
-- `Programmed`: desired state is realized (shadow exists downstream; for downstream agent, recordsets applied to backend)
-
-### Configuration CRD (server config)
-- `kind: DNSOperator` (internal config consumed by the binary via `--server-config`)
- - `discovery.mode`: `single` or `milo`
- - `downstreamResourceManagement.kubeconfigPath`: path inside the Pod to the downstream kubeconfig
- - `controllers.dnsRecordSetPowerDNS.maxConcurrentReconciles`: concurrent reconciles for the PowerDNS recordset controller (default: 4)
- - `controllers.dnsRecordSetPowerDNS.rateLimiterBaseDelay`: exponential backoff base delay (default: `1s`)
- - `controllers.dnsRecordSetPowerDNS.rateLimiterMaxDelay`: exponential backoff max delay (default: `30s`)
-
-### Development
-- Build: `make docker-build` (see `Makefile`)
-- Generate code/manifests: `make generate` and `make manifests`
-- Local e2e: see `test/e2e/chainsaw-test.yaml` and sample manifests under `config/samples/`
+
+The replicator mirrors upstream `DNSZone` / `DNSRecordSet` resources downstream
+and synthesizes their status back upstream. See
+[Replication Model](docs/architecture/replication.md).
+
+## Development
+
+- **Build:** `make docker-build` (see the [`Makefile`](Makefile))
+- **Generate code / manifests:** `make generate` and `make manifests`
+- **End-to-end tests:** see `test/e2e/` and the samples under `config/samples/`
diff --git a/docs/Taskfile.yaml b/docs/Taskfile.yaml
new file mode 100644
index 0000000..5bcbe52
--- /dev/null
+++ b/docs/Taskfile.yaml
@@ -0,0 +1,56 @@
+version: '3'
+
+# Renders the architecture diagrams from PlantUML sources using Docker,
+# mirroring the datum-cloud/enhancements convention. Each diagram is committed
+# next to the document that embeds it. The shared brand theme
+# (datum-theme*.puml) is an include, not a diagram, so it is excluded.
+#
+# The whole docs/architecture tree is mounted so relative includes (e.g.
+# ../datum-theme.puml from backends/) resolve, and each PNG is written back
+# beside its source.
+
+vars:
+ ARCH_DIR: "{{.USER_WORKING_DIR}}/docs/architecture"
+ OUTPUT_FORMAT: "png"
+ PLANTUML_IMAGE: "plantuml/plantuml:latest"
+
+tasks:
+ diagrams:
+ desc: Render all architecture diagrams from PlantUML
+ cmds:
+ - task: diagrams:render
+
+ diagrams:render:
+ desc: Render PlantUML diagrams to PNG using Docker
+ cmds:
+ - |
+ set -e
+ cd "{{.ARCH_DIR}}"
+ find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do
+ echo "Rendering: $puml"
+ docker run --rm -v "$(pwd)":/data "{{.PLANTUML_IMAGE}}" \
+ -t{{.OUTPUT_FORMAT}} -o "/data/$(dirname "$puml")" "/data/$puml"
+ done
+
+ diagrams:validate:
+ desc: Validate PlantUML syntax using Docker
+ cmds:
+ - |
+ set -e
+ cd "{{.ARCH_DIR}}"
+ find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do
+ echo "Validating: $puml"
+ docker run --rm -v "$(pwd)":/data "{{.PLANTUML_IMAGE}}" \
+ -syntax "/data/$puml"
+ done
+
+ diagrams:clean:
+ desc: Remove generated diagram PNGs
+ cmds:
+ - |
+ set -e
+ cd "{{.ARCH_DIR}}"
+ find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do
+ rm -f "${puml%.puml}.{{.OUTPUT_FORMAT}}"
+ done
+ echo "Removed generated diagrams"
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
new file mode 100644
index 0000000..9fda5ab
--- /dev/null
+++ b/docs/architecture/README.md
@@ -0,0 +1,157 @@
+# DNS Service Architecture
+
+The DNS service provides multi-tenant, Kubernetes-native authoritative DNS.
+Platform users declare zones and records as ordinary Kubernetes resources in
+their own control plane; the service carries that desired state down to an
+authoritative DNS backend and publishes the resulting records to a globally
+distributed serving layer.
+
+The **DNS operator** is the control-plane component at the heart of the service.
+It reconciles the user's resources and programs the backend. The DNS backend, the
+serving layer, and a shared state store make up the rest of the system, and this
+document describes how the operator and those components fit together.
+
+## How It Works
+
+Users author three kinds of resources: a `DNSZoneClass` that names the backend
+and nameserver policy, a `DNSZone` for each domain, and `DNSRecordSet` resources
+for the records within a zone. These resources live in the user's own **tenant
+control plane**, so they inherit the platform's identity, RBAC, and audit
+controls.
+
+The operator runs in one of two roles that split the work across trust
+boundaries:
+
+- A **replicator** watches tenant control planes, mirrors the desired state into
+ a shared authoritative cluster, and synthesizes status back up so users can
+ see whether their DNS is live.
+- A **downstream agent** runs next to the DNS backend, translates records into
+ backend calls, and owns the authoritative zone data.
+
+A read-only **serving layer** then replicates the authoritative data and answers
+live DNS queries close to end users.
+
+This separation keeps tenants off the DNS backend, hides the backend technology
+and cluster topology, and gives the authoritative data a single writer.
+
+## System Context
+
+
+
+
+
+The DNS service exists so platform users and other platform services can make the
+domains they own resolvable on the internet. Platform capabilities depend on it:
+networking and ingress publish records to expose workloads, and TLS certificate
+issuance validates domain control through DNS. A domain earns authority through
+the platform's **Domains** capability, which handles ownership and verification;
+the DNS service then serves that domain's records.
+
+Users and platform services author zones and records in a tenant control plane,
+which holds the desired state. The operator reads that state, programs the
+authoritative backend, and writes status back to the tenant control plane.
+Recursive resolvers query the serving layer directly over standard DNS, so the
+wider internet can reach the user's applications. The
+[Technology Stack](#technology-stack) section lists the open-source components
+that make up the service.
+
+## Core Concepts
+
+### Zones, Records, and Zone Classes
+
+- **[`DNSZone`](./api-reference.md#dnszone)** models a single domain. Its status
+ reports the authoritative nameservers and readiness.
+- **[`DNSRecordSet`](./api-reference.md#dnsrecordset)** models records of one type
+ within a zone, across one or more owner names (A, AAAA, CNAME, ALIAS, TXT, MX,
+ SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB).
+- **[`DNSZoneClass`](./api-reference.md#dnszoneclass)** is a cluster-scoped policy,
+ analogous to a `StorageClass`, that selects the **backend** (via
+ `controllerName`) and the **nameserver policy** for every zone that references
+ it. The class is the seam that keeps backend choice out of individual zones.
+
+See the [API Reference](./api-reference.md) for the full resource schema.
+
+### Backends
+
+A `DNSZoneClass` selects a backend by name through `spec.controllerName`. The
+downstream agent acts only on zones whose class names a backend it implements, so
+one deployment can host several classes and several backends side by side.
+**PowerDNS** is the backend the operator implements today. The record-translation
+and zone-management logic sit behind a narrow backend interface, so you can add
+authoritative servers without changing the reconcilers.
+
+See [DNS Backends](./backends/README.md) for the backend model and the
+[PowerDNS backend](./backends/powerdns.md) for that backend's architecture.
+
+### Multi-Tenancy via Control Plane Discovery
+
+The operator isolates tenants by control plane rather than by namespace
+convention. The replicator **discovers** the control planes it serves and
+reconciles each one independently. Two discovery modes exist:
+
+- **`single`** — the operator serves one upstream cluster, the cluster it runs
+ in. This mode suits self-contained deployments and development.
+- **`milo`** — the operator discovers per-project control planes from a platform
+ control plane and connects to each one. This mode serves the multi-tenant
+ platform.
+
+Because the desired state lives in the tenant's own control plane, each tenant
+sees only its own zones and records, and the operator never exposes its
+authoritative cluster to tenants.
+
+### Status Synthesis and Conditions
+
+Every zone and record resource carries two conditions that the operator sets:
+
+- **`Accepted`** — the resource is valid and its dependencies are satisfied.
+- **`Programmed`** — the backend has realized the desired state.
+
+> [!NOTE]
+>
+> The one-shot `DNSZoneDiscovery` uses `Accepted` and `Discovered` instead of
+> `Programmed`.
+
+The replicator mirrors realized status from the authoritative cluster back to the
+tenant control plane. A user watching a `DNSZone` therefore sees `Programmed=True`
+only after the zone actually serves. See [Replication Model](./replication.md)
+for how the operator synthesizes status across the two clusters.
+
+## Technology Stack
+
+| Component | Technology | Purpose |
+|-----------|------------|---------|
+| **API model** | [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) | Zones, records, and zone classes as native Kubernetes objects |
+| **Controller runtime** | [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) + [multicluster-runtime](https://github.com/kubernetes-sigs/multicluster-runtime) | Reconciliation across one or many control planes |
+| **Authoritative backend** | [PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/) | Serves authoritative zone data |
+| **State replication** | [LightningStream](https://github.com/PowerDNS/lightningstream) + object storage | Replicates the authoritative LMDB store to the serving layer |
+
+## API Resources
+
+The operator serves these resources under `dns.networking.miloapis.com/v1alpha1`:
+
+| Resource | Scope | Description |
+|----------|-------|-------------|
+| `DNSZoneClass` | Cluster | Selects backend and nameserver policy for zones |
+| `DNSZone` | Namespaced | A single authoritative domain |
+| `DNSRecordSet` | Namespaced | Records of one type for one or more owner names in a zone |
+| `DNSZoneDiscovery` | Namespaced | One-shot snapshot of a zone's live records |
+
+See the [API Reference](./api-reference.md) for complete field documentation.
+
+## Learn More
+
+- [Deployment Topology](./topology.md) — Roles, control planes, and the serving
+ layer
+- [Replication Model](./replication.md) — Shadow objects, namespace mapping, and
+ status synthesis
+- [DNS Backends](./backends/README.md) — Backend model and available backends,
+ including the [PowerDNS backend](./backends/powerdns.md)
+- [API Reference](./api-reference.md) — Full resource schema and conditions
+
+## References
+
+- [PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/)
+- [Kubernetes Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
+- [multicluster-runtime](https://github.com/kubernetes-sigs/multicluster-runtime)
+- [LightningStream](https://github.com/PowerDNS/lightningstream)
+- [C4 model](https://c4model.com) — notation used for the diagrams in these docs
diff --git a/docs/architecture/api-reference.md b/docs/architecture/api-reference.md
new file mode 100644
index 0000000..0440641
--- /dev/null
+++ b/docs/architecture/api-reference.md
@@ -0,0 +1,147 @@
+# API Reference
+
+This reference documents the user-facing API — the resources platform users
+create and read to manage DNS. The operator serves them under the API
+group/version **`dns.networking.miloapis.com/v1alpha1`**. Generated CRDs live in
+[`config/crd/bases`](../../config/crd/bases); runnable samples live in
+[`config/samples`](../../config/samples).
+
+| Resource | Scope | Purpose |
+|----------|-------|---------|
+| [`DNSZoneClass`](#dnszoneclass) | Cluster | Selects backend and nameserver policy |
+| [`DNSZone`](#dnszone) | Namespaced | A single authoritative domain |
+| [`DNSRecordSet`](#dnsrecordset) | Namespaced | Records of one type, for one or more owner names |
+| [`DNSZoneDiscovery`](#dnszonediscovery) | Namespaced | One-shot snapshot of live records |
+
+## DNSZoneClass
+
+Cluster-scoped policy, analogous to a `StorageClass`. Every `DNSZone` references
+a class, which determines the backend and how the operator assigns authoritative
+nameservers.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `spec.controllerName` | string | Backend selector (e.g. `powerdns`). The downstream agent only acts on zones whose class it implements. |
+| `spec.nameServerPolicy.mode` | string | Nameserver assignment mode. `Static` is currently supported. |
+| `spec.nameServerPolicy.static.servers` | []string | Authoritative nameservers advertised for zones using this class. |
+| `spec.defaults.defaultTTL` | int64 | Optional default TTL applied to zones. |
+| `status.conditions` | []Condition | `Accepted`, `Programmed`. |
+
+```yaml
+apiVersion: dns.networking.miloapis.com/v1alpha1
+kind: DNSZoneClass
+metadata:
+ name: powerdns
+spec:
+ controllerName: powerdns
+ nameServerPolicy:
+ mode: Static
+ static:
+ servers: ["ns1.example.net.", "ns2.example.net."]
+```
+
+## DNSZone
+
+Namespaced. Models a single domain.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `spec.domainName` | string | Required FQDN (e.g. `example.com`). Immutable once set. |
+| `spec.dnsZoneClassName` | string | Reference to a `DNSZoneClass`. |
+| `status.nameservers` | []string | Authoritative nameservers, derived from the class policy. |
+| `status.recordCount` | int | Number of record sets in the zone. |
+| `status.conditions` | []Condition | `Accepted`, `Programmed`. |
+| `status.domainRef` | object | Link to the owning `Domain`, exposing its name and assigned nameservers, when present. |
+
+```yaml
+apiVersion: dns.networking.miloapis.com/v1alpha1
+kind: DNSZone
+metadata:
+ name: example-com
+ namespace: default
+spec:
+ domainName: example.com
+ dnsZoneClassName: powerdns
+```
+
+## DNSRecordSet
+
+Namespaced. Models records of one record type within a zone, across one or more
+owner names. Each entry in `spec.records` sets one owner name and carries exactly
+one typed field matching `spec.recordType`; the backend groups entries that share
+an owner name into a single RRset.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `spec.dnsZoneRef` | LocalObjectReference | The `DNSZone` in the same namespace. |
+| `spec.recordType` | string | One of `A, AAAA, ALIAS, CNAME, TXT, MX, SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB`. |
+| `spec.records[].name` | string | Owner name; `@` for the zone apex. |
+| `spec.records[].ttl` | int64 | Optional per-owner TTL. |
+| `spec.records[].` | object | Typed record content for the entry. Each typed field's `content` is a single value (e.g. `a.content: "192.0.2.10"`); other types use their own fields (`mx.preference`/`mx.exchange`, `srv.*`, `soa.*`). |
+| `status.conditions` | []Condition | `Accepted`, `Programmed`. |
+| `status.recordSets[]` | []object | Per-owner-name realized status, including per-record `Programmed`. |
+
+Each entry holds a single value. To give one owner name several addresses, add
+one entry per value; the backend groups them into one RRset:
+
+```yaml
+apiVersion: dns.networking.miloapis.com/v1alpha1
+kind: DNSRecordSet
+metadata:
+ name: www-a
+ namespace: default
+spec:
+ dnsZoneRef:
+ name: example-com
+ recordType: A
+ records:
+ - name: www
+ a:
+ content: "192.0.2.10"
+ ttl: 300
+ - name: www
+ a:
+ content: "192.0.2.11"
+ ttl: 300
+```
+
+### Multi-owner conflict resolution
+
+When several `DNSRecordSet` resources target the same zone, owner name, and
+record type, the agent programs a **single** owner (chosen by oldest creation
+timestamp, then name). The agent marks the others `Programmed=False` with reason
+`NotOwner`, so conflicting records never silently overwrite each other.
+
+## DNSZoneDiscovery
+
+Namespaced, write-once. Snapshots a zone's live records via DNS queries; performs
+no backend writes. Useful for onboarding or verifying an existing domain.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `spec.dnsZoneRef` | LocalObjectReference | The `DNSZone` to snapshot. |
+| `status.conditions` | []Condition | `Accepted`, `Discovered`. |
+| `status.recordSets[]` | []object | Discovered records, grouped by record type. |
+
+## Conditions
+
+Every DNS resource reports status through standard Kubernetes conditions:
+
+| Condition | Meaning |
+|-----------|---------|
+| `Accepted` | The resource is valid and its dependencies are satisfied. |
+| `Programmed` | The backend has realized the desired state. |
+| `Discovered` | (`DNSZoneDiscovery` only) The live-record snapshot completed. |
+
+Common reasons include `Pending`, `Programmed`, `DNSZoneInUse` (domain already
+claimed by another zone), `NotOwner` (a conflicting record set owns the name),
+and `PDNSError` (the backend rejected the change). See
+[Replication Model](./replication.md#status-synthesis) for how the operator
+synthesizes conditions across clusters.
+
+> [!NOTE]
+>
+> This reference covers the user-facing API. The `DNSOperator` object that
+> configures the operator binary is deployment configuration, not part of the
+> served API — see [Deployment Topology → Operator
+> Configuration](./topology.md#operator-configuration).
diff --git a/docs/architecture/backends/README.md b/docs/architecture/backends/README.md
new file mode 100644
index 0000000..6a01485
--- /dev/null
+++ b/docs/architecture/backends/README.md
@@ -0,0 +1,67 @@
+# DNS Backends
+
+A **backend** is the authoritative DNS server that the downstream agent programs
+and that answers queries. The operator keeps backend choice out of individual
+zones: a `DNSZoneClass` names a backend, and every `DNSZone` that references the
+class uses it. This design lets the platform offer several backends and lets you
+add new ones without changing the DNS API.
+
+## Backend Model
+
+The downstream agent talks to a backend through a Go client. A narrow interface
+covers the two record operations — replace the records for one owner name and
+type, and delete them — so the record reconciler stays backend-agnostic. Zone
+operations (create, read, delete) are methods on the backend's concrete client
+rather than part of that interface. Either way, the reconcilers call the
+backend's Go client and never its native API directly.
+
+Three properties hold for every backend:
+
+- **Selection by name.** A `DNSZoneClass` sets `spec.controllerName` (for
+ example, `powerdns`). The agent acts only on zones whose class names a backend
+ it implements and ignores every other class. One deployment can therefore host
+ several classes and several backends at once.
+- **Single writer.** For each (zone, record type, owner name) tuple, the agent
+ programs exactly one owner. When several `DNSRecordSet` resources target the
+ same tuple, the agent picks one owner and marks the rest `Programmed=False`
+ with reason `NotOwner`, so conflicting records never overwrite each other.
+- **Authoritative reconciliation.** The agent treats the desired state as
+ authoritative. It replaces the owners a zone should have and deletes owners of
+ the same type that no longer belong, so the backend converges on the declared
+ records.
+
+Because these properties live in the agent rather than in any one backend, every
+backend behaves consistently from the user's point of view. A user picks a
+`DNSZoneClass`; the choice of authoritative server behind it stays an operator
+concern.
+
+## Available Backends
+
+| Backend | `controllerName` | Status | Documentation |
+|---------|------------------|--------|---------------|
+| [PowerDNS](./powerdns.md) | `powerdns` | Implemented | [PowerDNS backend](./powerdns.md) |
+
+The platform expects to offer more backends over time. Each new backend adds a
+row here and a companion page that documents its specifics.
+
+## Adding a Backend
+
+Adding a backend is an operator task, not an API change. At a high level:
+
+1. Implement the record interface and the zone-management client for the target
+ authoritative server.
+2. Choose a `controllerName` and register the backend under it.
+3. Package the server and its dependencies in a deployment overlay, following the
+ pattern in [`config/agent`](../../../config/agent).
+4. Add a companion page under this directory and a row to
+ [Available Backends](#available-backends).
+
+The API types, replicator, zone-class model, and status conditions stay
+unchanged, so existing zones and clients keep working.
+
+## Related
+
+- [Architecture Overview](../README.md) — Zone classes and the backend concept
+- [Deployment Topology](../topology.md) — Where a backend and its serving layer
+ run
+- [API Reference](../api-reference.md#dnszoneclass) — `DNSZoneClass` schema
diff --git a/docs/architecture/backends/powerdns-backend.png b/docs/architecture/backends/powerdns-backend.png
new file mode 100644
index 0000000..a0e025d
Binary files /dev/null and b/docs/architecture/backends/powerdns-backend.png differ
diff --git a/docs/architecture/backends/powerdns-backend.puml b/docs/architecture/backends/powerdns-backend.puml
new file mode 100644
index 0000000..c695446
--- /dev/null
+++ b/docs/architecture/backends/powerdns-backend.puml
@@ -0,0 +1,43 @@
+@startuml powerdns-backend
+!$NEW_C4_STYLE = 1
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml
+!include ../datum-theme.puml
+
+LAYOUT_LEFT_RIGHT()
+
+title Deployment View — PowerDNS Backend
+
+Deployment_Node(rcp, "Replication Control Plane", "Kubernetes cluster") {
+ Container(replicator, "DNS Operator (replicator)", "controller-runtime", "Mirrors desired state into the authoritative cluster")
+}
+
+Deployment_Node(auth, "Authoritative Cluster", "Kubernetes cluster") {
+ Container(api, "Kubernetes API", "control plane", "Holds shadow DNSZone / DNSRecordSet")
+ Container(agent, "DNS Operator (downstream agent)", "controller-runtime", "Reconciles shadows and programs PowerDNS")
+ Deployment_Node(pod, "pdns-auth", "StatefulSet pod") {
+ Container(pdns, "PowerDNS Authoritative", "pdns-auth + LMDB", "Writer and source of truth")
+ Container(pub, "LightningStream", "sidecar", "Snapshots LMDB to object storage")
+ }
+}
+
+ContainerDb(store, "Object Storage", "S3-compatible", "Durable authoritative state")
+
+Deployment_Node(edge, "Serving Layer", "Edge nodes") {
+ Deployment_Node(node, "Serving Node", "DaemonSet pod") {
+ Container(spdns, "PowerDNS (read-only)", "pdns-auth + LMDB", "Answers DNS queries")
+ Container(recv, "LightningStream", "sidecar (receive)", "Pulls snapshots from object storage")
+ Container(rec, "Recursor", "pdns-recursor", "Expands ALIAS at query time")
+ }
+}
+
+Person_Ext(resolver, "Recursive Resolver", "Resolves names for the internet")
+
+Rel(replicator, api, "Writes shadow DNS resources", "apply")
+Rel(agent, api, "Watches shadows, writes status", "watch")
+Rel(agent, pdns, "Programs zones / records", "HTTP API + X-API-Key")
+Rel(pub, store, "Writes snapshots")
+Rel(recv, store, "Pulls snapshots")
+Rel(resolver, spdns, "DNS query", "UDP/TCP :53")
+
+SHOW_LEGEND()
+@enduml
diff --git a/docs/architecture/backends/powerdns.md b/docs/architecture/backends/powerdns.md
new file mode 100644
index 0000000..fef0c1d
--- /dev/null
+++ b/docs/architecture/backends/powerdns.md
@@ -0,0 +1,133 @@
+# PowerDNS Backend
+
+[PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/) is the
+backend the operator implements today. A zone opts in by referencing a
+`DNSZoneClass` with `controllerName: powerdns`. This page documents how the
+PowerDNS backend translates records, programs zones, stores data, and serves
+queries. For the properties every backend shares, see the
+[backend model](./README.md#backend-model).
+
+The deployment view below shows where each component runs: the downstream agent
+and the PowerDNS writer in the authoritative cluster (fed shadow resources by the
+replicator through the cluster's Kubernetes API), and the read-only serving nodes
+at the edge, replicating through shared object storage.
+
+
+
+
+
+## Record Translation
+
+The PowerDNS client translates each typed `DNSRecordSet` entry into the
+presentation format PowerDNS expects. Translation handles the details that DNS
+record types require:
+
+- Synthesizes a date-based `SOA` serial (`YYYYMMDD01`) when a record omits it.
+- Encodes `SVCB` and `HTTPS` service parameters.
+- Quotes and escapes `TXT` content.
+- Qualifies owner names against the zone and removes duplicate values.
+
+## Zone and Record Programming
+
+The client drives PowerDNS through its HTTP API, authenticating with an API key
+in the `X-API-Key` header. For a zone, the agent ensures the zone exists and
+applies the nameserver policy from its `DNSZoneClass`. For a record set, the
+agent replaces the desired owners and deletes extraneous owners of the same
+type. When PowerDNS rejects a change, the agent reports the failure on the
+resource with reason `PDNSError` and a human-readable message.
+
+## Storage and Serving
+
+PowerDNS stores authoritative zone data in an embedded **LMDB** database. To
+serve queries close to end users, the deployment replicates that database rather
+than the query path:
+
+1. A [LightningStream](https://github.com/PowerDNS/lightningstream) sidecar
+ snapshots the LMDB store to shared, S3-compatible object storage.
+2. Each serving node runs a read-only PowerDNS with a LightningStream sidecar
+ that pulls the snapshots and answers queries.
+
+Serving nodes hold only their local replica, so the deployment can add nodes
+anywhere object storage is reachable. A serving node can also run a local
+recursor to expand `ALIAS` records at query time. See
+[Deployment Topology](../topology.md#authoritative-serving-and-state-replication)
+for how the serving layer fits the wider system.
+
+## Propagation and Timing
+
+A record change reaches end users through four stages. Each stage adds delay, and
+a different setting governs each one, so it helps to reason about them separately.
+
+| Stage | What happens | What governs the delay |
+|-------|--------------|------------------------|
+| 1. Reconcile | The agent programs the change into the writer's PowerDNS through the HTTP API. | Controller queue latency, normally seconds. On a backend error the agent retries with exponential backoff, from `rateLimiterBaseDelay` (1s) up to `rateLimiterMaxDelay` (30s). |
+| 2. Authoritative write | The writer's PowerDNS serves the change. It reads LMDB directly (`zone-cache-refresh-interval=0`), so no cache stands between the write and the answer. | Effectively immediate on the writer. |
+| 3. Replicate to serving nodes | A LightningStream sidecar snapshots the LMDB to object storage; each serving node lists object storage, downloads new snapshots, and merges them. | The writer's `lmdb_poll_interval` plus each node's `storage_poll_interval` (see [LightningStream intervals](#lightningstream-intervals)), plus object-storage upload and download time — typically a few seconds. |
+| 4. Resolver caching | Recursive resolvers and clients cache the answer until its TTL expires. They also cache a *missing* name for the zone's negative-cache TTL (the `SOA` minimum). | The record's TTL, and — for a newly added name — the negative-cache TTL of any earlier lookup. |
+
+Stages 1–3 move a change from the API to every authoritative serving node,
+typically within a few seconds. Stage 4 dominates what end users observe, because
+a resolver keeps serving a cached answer until the TTL expires regardless of how
+fast the authoritative servers update.
+
+### LightningStream intervals
+
+LightningStream drives stage 3. The deployment runs it with default intervals, so
+a change reaches every serving node within a few seconds of becoming
+authoritative on the writer, bounded mainly by object-storage latency:
+
+| Setting | Default | Effect on propagation |
+|---------|---------|-----------------------|
+| `lmdb_poll_interval` | `1s` | How often the writer's sidecar checks LMDB for changes to snapshot. |
+| `storage_poll_interval` | `1s` | How often a serving node lists object storage for new snapshots. |
+| `storage_force_snapshot_interval` | `4h` | Writes a snapshot even with no changes, so an idle instance does not look stale. |
+| `storage_retry_interval` | `5s` | Retry delay after a failed snapshot upload or download. |
+
+Lowering the poll intervals shortens propagation at the cost of more frequent
+storage listings; raising them does the reverse. These intervals govern only how
+fast a change reaches the serving nodes — end users still see it no sooner than
+their cached TTL allows.
+
+Two practical consequences follow:
+
+- **To make a change take effect quickly, lower the record's TTL before you make
+ it.** Set a short TTL, wait for the old TTL to expire everywhere, change the
+ record, then raise the TTL again. The serving pipeline itself adds only seconds
+ to minutes; the TTL sets the ceiling.
+- **A brand-new name can appear slowly if something looked it up first**, because
+ resolvers cached the negative answer for the zone's negative-cache TTL. The
+ operator's default `SOA` sets this value (see
+ [Replication Model](../replication.md#default-soa-and-ns-records)).
+
+## Configuration
+
+The agent connects to PowerDNS through environment variables:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `PDNS_API_URL` | `http://127.0.0.1:8081` | PowerDNS HTTP API endpoint. |
+| `PDNS_API_KEY` | — | API key (or use `PDNS_API_KEY_FILE`). |
+| `PDNS_API_KEY_FILE` | — | Path to a file that contains the API key. |
+
+The PowerDNS record-set controller adds its own tuning under
+`controllers.dnsRecordSetPowerDNS` in the [`DNSOperator` server
+config](../topology.md#operator-configuration):
+
+| Field | Default | Description |
+|-------|---------|-------------|
+| `controllers.dnsRecordSetPowerDNS.maxConcurrentReconciles` | `4` | Concurrent reconciles for the PowerDNS record-set controller. |
+| `controllers.dnsRecordSetPowerDNS.rateLimiterBaseDelay` | `1s` | Exponential backoff base delay. |
+| `controllers.dnsRecordSetPowerDNS.rateLimiterMaxDelay` | `30s` | Exponential backoff max delay. |
+
+The [`config/agent`](../../../config/agent) base bundles PowerDNS, the recursor,
+and LightningStream alongside the agent and wires the API key through a shared
+volume; the runnable [`config/overlays/agent-powerdns`](../../../config/overlays/agent-powerdns)
+overlay adds the namespace and a storage backend. See
+[Deployment Topology](../topology.md) for the deployment shape.
+
+## Related
+
+- [DNS Backends](./README.md) — The backend model that every backend shares
+- [API Reference](../api-reference.md#dnszoneclass) — `DNSZoneClass` schema
+- [Deployment Topology](../topology.md#operator-configuration) — The generic
+ `DNSOperator` server config
diff --git a/docs/architecture/container-view.png b/docs/architecture/container-view.png
new file mode 100644
index 0000000..599fbb2
Binary files /dev/null and b/docs/architecture/container-view.png differ
diff --git a/docs/architecture/container-view.puml b/docs/architecture/container-view.puml
new file mode 100644
index 0000000..7829e1c
--- /dev/null
+++ b/docs/architecture/container-view.puml
@@ -0,0 +1,38 @@
+@startuml container-view
+!$NEW_C4_STYLE = 1
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+!include datum-theme.puml
+
+LAYOUT_LEFT_RIGHT()
+
+title Container View — DNS Service
+
+Person(user, "Platform User", "Declares zones and records")
+Person(resolver, "Recursive Resolver", "Resolves names for the internet")
+
+System_Boundary(tenant, "Tenant Control Planes") {
+ Container(cp, "Project Control Plane", "Kubernetes API", "Holds user-authored DNSZone / DNSRecordSet / DNSZoneClass")
+}
+
+System_Boundary(replication, "Replication Control Plane") {
+ Container(replicator, "DNS Operator (replicator)", "Go / controller-runtime", "Discovers control planes, mirrors desired state, synthesizes status")
+}
+
+System_Boundary(authoritative, "Authoritative Cluster") {
+ Container(agent, "DNS Operator (downstream agent)", "Go / controller-runtime", "Programs zones and records")
+ Container(pdns, "Authoritative Backend", "DNS server", "Source of truth for authoritative zone data")
+}
+
+System_Boundary(edge, "Serving Layer") {
+ Container(serving, "Authoritative Serving Nodes", "DNS servers", "Answer public DNS queries")
+}
+
+Rel(user, cp, "Creates / edits DNS resources", "kubectl / API")
+Rel(replicator, cp, "Watches spec, writes status", "watch / patch")
+Rel(replicator, agent, "Mirrors desired state as shadow objects")
+Rel(agent, pdns, "Programs zones / records")
+Rel(pdns, serving, "Replicates authoritative zone data", "backend-specific")
+Rel_U(resolver, serving, "DNS query", "UDP/TCP :53")
+
+SHOW_LEGEND()
+@enduml
diff --git a/docs/architecture/datum-theme.puml b/docs/architecture/datum-theme.puml
new file mode 100644
index 0000000..a1171be
--- /dev/null
+++ b/docs/architecture/datum-theme.puml
@@ -0,0 +1,55 @@
+' Datum Cloud C4 Theme (Modern Style)
+' Based on brand guidelines: https://www.datum.net/brand/color
+'
+' IMPORTANT: Set !$NEW_C4_STYLE = 1 BEFORE including C4 libraries in your diagram.
+' This theme should be included AFTER C4 library includes.
+
+' Brand Colors
+!$DATUM_MIDNIGHT_FJORD = "#0C1D31"
+!$DATUM_AURORA_MOSS = "#E6F59F"
+!$DATUM_CANYON_CLAY = "#BF9595"
+!$DATUM_PINE_FORGE = "#4D6356"
+!$DATUM_BLUSH_QUARTZ = "#ECD0D0"
+!$DATUM_GLACIER_MIST = "#F6F6F5"
+!$DATUM_GLACIER_MIST_DARK = "#E8E7E4"
+
+' Element Background Colors - Internal (vibrant pastels)
+!$DATUM_BG_PERSON = "#F5D5D5"
+!$DATUM_BG_SYSTEM = "#D5E5F5"
+!$DATUM_BG_CONTAINER = "#D5F5E0"
+!$DATUM_BG_COMPONENT = "#F5F0D5"
+
+' Element Background Colors - External (muted/grayed)
+!$DATUM_BG_EXT_PERSON = "#E8E0E0"
+!$DATUM_BG_EXT_SYSTEM = "#E0E8EC"
+!$DATUM_BG_EXT_CONTAINER = "#E0ECE4"
+!$DATUM_BG_EXT_COMPONENT = "#ECE8E0"
+
+' Person elements - Canyon Clay accents, pink background
+UpdateElementStyle("person", $bgColor=$DATUM_BG_PERSON, $borderColor=$DATUM_CANYON_CLAY, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateElementStyle("external_person", $bgColor=$DATUM_BG_EXT_PERSON, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE)
+
+' System elements - blue background
+UpdateElementStyle("system", $bgColor=$DATUM_BG_SYSTEM, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateElementStyle("external_system", $bgColor=$DATUM_BG_EXT_SYSTEM, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE)
+
+' Container elements - green background
+UpdateElementStyle("container", $bgColor=$DATUM_BG_CONTAINER, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateElementStyle("external_container", $bgColor=$DATUM_BG_EXT_CONTAINER, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE)
+
+' Component elements - yellow background
+UpdateElementStyle("component", $bgColor=$DATUM_BG_COMPONENT, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateElementStyle("external_component", $bgColor=$DATUM_BG_EXT_COMPONENT, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE)
+
+' Boundary styling - subtle Glacier Mist background
+UpdateBoundaryStyle("system", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateBoundaryStyle("container", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+UpdateBoundaryStyle("enterprise", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD)
+
+' Relationship styling
+UpdateRelStyle("", $textColor=$DATUM_MIDNIGHT_FJORD, $lineColor=$DATUM_MIDNIGHT_FJORD)
+
+' Legend inherits from element styles via skinparam
+skinparam LegendBackgroundColor $DATUM_GLACIER_MIST
+skinparam LegendBorderColor $DATUM_MIDNIGHT_FJORD
+skinparam LegendFontColor $DATUM_MIDNIGHT_FJORD
diff --git a/docs/architecture/replication-flow.png b/docs/architecture/replication-flow.png
new file mode 100644
index 0000000..83c9d44
Binary files /dev/null and b/docs/architecture/replication-flow.png differ
diff --git a/docs/architecture/replication-flow.puml b/docs/architecture/replication-flow.puml
new file mode 100644
index 0000000..8b7c0a5
--- /dev/null
+++ b/docs/architecture/replication-flow.puml
@@ -0,0 +1,25 @@
+@startuml replication-flow
+!$NEW_C4_STYLE = 1
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Dynamic.puml
+!include datum-theme.puml
+
+title Replication Flow — Desired State Down, Realized Status Up
+
+System_Boundary(up, "Upstream Control Plane") {
+ Container(uz, "DNSZone / DNSRecordSet", "Kubernetes resource", "User-authored desired state")
+}
+
+System_Boundary(down, "Authoritative Cluster") {
+ Container(sz, "Shadow DNSZone / DNSRecordSet", "Kubernetes resource", "Operator-managed mirror")
+ Container(agent, "Downstream Agent", "controller-runtime", "Programs the backend")
+ ContainerDb(be, "DNS Backend", "PowerDNS", "Authoritative zone data")
+}
+
+RelIndex(1, uz, sz, "Replicator mirrors spec", "down")
+RelIndex(2, sz, agent, "Agent reconciles shadow")
+RelIndex(3, agent, be, "Programs zones / records", "HTTP API")
+RelIndex(4, be, sz, "Realized status")
+RelIndex(5, sz, uz, "Replicator mirrors status", "up")
+
+SHOW_LEGEND()
+@enduml
diff --git a/docs/architecture/replication.md b/docs/architecture/replication.md
new file mode 100644
index 0000000..f68e960
--- /dev/null
+++ b/docs/architecture/replication.md
@@ -0,0 +1,97 @@
+# Replication Model
+
+The replicator bridges two trust boundaries: the **upstream** tenant control
+plane, where users author DNS resources, and the **downstream** authoritative
+cluster, where the DNS backend runs. It mirrors desired state down, realized
+status up, and never gives tenants access to the authoritative cluster.
+
+This document describes how that bridge works. For where each side runs, see
+[Deployment Topology](./topology.md).
+
+## Upstream and Downstream
+
+- **Upstream** — a tenant / project control plane (or, in `single` mode, the
+ local cluster) holding the user-facing `DNSZone` and `DNSRecordSet`.
+- **Downstream** — the shared authoritative cluster where the DNS backend and the
+ downstream agent run.
+
+For every upstream resource, the replicator maintains a **shadow object**
+downstream. The downstream agent programs the backend from the shadow and writes
+realized status onto it; the replicator mirrors that status back upstream. A
+change on either side wakes the replicator via cross-cluster watches.
+
+
+
+
+
+## Shadow Objects and Namespace Mapping
+
+Kubernetes does not support cross-cluster ownership references, so the replicator
+uses a **mapped-namespace** strategy to place and track shadows:
+
+- Each upstream namespace maps to a downstream namespace named from the upstream
+ namespace's UID (`ns-`), keeping tenant namespaces collision-free without
+ leaking their names.
+- The shadow keeps the **same object name** as its upstream source.
+- **Provenance annotations** (`meta.datumapis.com/upstream-*`) record the source
+ cluster, group, kind, name, and namespace on every shadow.
+- An **anchor** object downstream stands in for the missing cross-cluster owner
+ reference, so garbage collection cleans up shadows when the upstream resource
+ is deleted.
+
+Deletion flows downstream-first: the replicator removes the shadow (and its
+backend state) before releasing the upstream finalizer, so a deleted zone stops
+resolving before it disappears from the tenant's view.
+
+## Status Synthesis
+
+The replicator computes the two conditions users watch, combining upstream
+validity with downstream reality:
+
+| Condition | Set to `True` when |
+|-----------|--------------------|
+| `Accepted` | The resource is valid, its dependencies exist, and (for zones) the operator knows the authoritative nameservers |
+| `Programmed` | The backend has realized the desired state — for a zone, its default `SOA` and `NS` record sets exist; for a record set, the backend has applied it |
+
+The replicator mirrors record-set status straight from the downstream shadow,
+including per-owner results, so a user sees exactly what the backend realized.
+Zone status also reports the authoritative nameservers and a record count.
+
+## Zone Ownership Accounting
+
+Because many tenant control planes feed one authoritative cluster, two tenants
+could request the same domain. The replicator guards against that collision with
+an **ownership ledger** in the authoritative cluster, keyed by domain name. The
+first zone to claim a domain wins. The replicator holds a later claimant's zone
+with `Accepted=False` (reason `DNSZoneInUse`) and emits a warning event, rather
+than overwriting the incumbent. The replicator releases ownership when the owning
+zone is deleted.
+
+## Default SOA and NS Records
+
+A zone is not authoritative until it has `SOA` and `NS` records. Once the
+operator knows the zone's nameservers (from its `DNSZoneClass` nameserver
+policy), the replicator ensures two operator-managed record sets exist for the
+apex (`@`):
+
+- an **`NS`** set that lists the authoritative nameservers, and
+- an **`SOA`** set whose primary nameserver is the first of those, with a
+ hostmaster address derived from the domain and default refresh, retry, and
+ expire values.
+
+The replicator creates these sets only when they are missing, so it never
+overwrites user-authored apex records. The operator synthesizes a date-based
+`SOA` serial (`YYYYMMDD01`) when a record omits one.
+
+## Discovery of Zone Records
+
+`DNSZoneDiscovery` is a one-shot, read-only companion resource. The replicator
+resolves the referenced zone's live records over DNS and writes them into the
+resource's status. Use it to snapshot existing DNS during onboarding or
+verification; it performs no mirroring and no backend writes.
+
+## Related
+
+- [Deployment Topology](./topology.md) — Where upstream, downstream, and serving
+ layers run
+- [API Reference](./api-reference.md) — Conditions and resource schema
diff --git a/docs/architecture/system-context.png b/docs/architecture/system-context.png
new file mode 100644
index 0000000..bbc388c
Binary files /dev/null and b/docs/architecture/system-context.png differ
diff --git a/docs/architecture/system-context.puml b/docs/architecture/system-context.puml
new file mode 100644
index 0000000..a668617
--- /dev/null
+++ b/docs/architecture/system-context.puml
@@ -0,0 +1,30 @@
+@startuml system-context
+!$NEW_C4_STYLE = 1
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
+!include datum-theme.puml
+
+LAYOUT_LEFT_RIGHT()
+
+title System Context — DNS Service
+
+Person(user, "Platform User", "Owns domains and publishes DNS so their applications are reachable")
+
+System_Boundary(dns, "DNS Service") {
+ System(operator, "DNS Operator", "Reconciles zones and records into an authoritative backend")
+ System(serving, "Authoritative Serving Layer", "Answers DNS queries")
+}
+
+System_Ext(domains, "Domains", "Domain ownership and verification")
+System_Ext(services, "Platform Services", "Networking, ingress, and TLS certificate issuance")
+System_Ext(cp, "Tenant Control Planes", "Kubernetes API where users and platform services author DNS resources")
+Person_Ext(resolver, "Recursive Resolver", "Resolves names for the internet")
+
+Rel_D(user, cp, "Declares zones and records", "kubectl / API")
+Rel_D(services, cp, "Publish records to expose workloads and validate domain control")
+Rel(domains, cp, "Supply verified domains that zones are created for")
+Rel(operator, cp, "Watches desired state, writes status", "watch / patch")
+Rel(operator, serving, "Programs authoritative zones")
+Rel_U(resolver, serving, "Resolves published records", "UDP/TCP :53")
+
+SHOW_LEGEND()
+@enduml
diff --git a/docs/architecture/topology.md b/docs/architecture/topology.md
new file mode 100644
index 0000000..fe72850
--- /dev/null
+++ b/docs/architecture/topology.md
@@ -0,0 +1,152 @@
+# Deployment Topology
+
+The DNS operator — the service's control-plane component — is a single binary
+that runs in one of two **roles**, which you select with `--role`. A complete
+deployment composes those roles across a few control planes and a serving fleet.
+This document describes the components a deployment needs, independent of any
+specific cloud or cluster technology.
+
+> [!NOTE]
+>
+> This is a generic reference topology. A given environment may collapse or
+> expand these tiers — for example, running everything in one cluster for
+> development (`discovery.mode: single`) or spreading the serving layer across
+> many regions in production.
+
+## Container View
+
+
+
+
+
+## Roles
+
+### Replicator
+
+**Runs in:** the replication control plane, one deployment per platform.
+
+**Responsibilities:**
+- Discover the tenant control planes to serve (see [Discovery](#discovery)).
+- Mirror each `DNSZone` / `DNSRecordSet` into the authoritative cluster as a
+ **shadow object**.
+- Ensure a default `SOA` and `NS` record set exist for each zone.
+- Account for zone ownership so two tenants cannot claim the same domain.
+- Synthesize `Accepted` / `Programmed` status back onto the tenant's resources.
+
+The replicator holds credentials for the authoritative cluster and for each
+control plane it discovers; tenants never receive access to the authoritative
+cluster. See [Replication Model](./replication.md) for the internals.
+
+### Downstream Agent
+
+**Runs in:** the authoritative cluster, co-located with the DNS backend.
+
+**Responsibilities:**
+- Ensure zones exist in the backend for classes it implements, applying the
+ class's nameserver policy.
+- Translate `DNSRecordSet` resources into authoritative record sets in the
+ backend, resolving multi-owner conflicts to a single writer.
+- Report realized status (`Programmed`, per-record results) on the shadow
+ objects, which the replicator mirrors upstream.
+
+The agent is the **only writer** to the DNS backend. It acts only on zones whose
+`DNSZoneClass.spec.controllerName` matches a backend it implements (`powerdns`
+today) and ignores every other class. See [DNS Backends](./backends/README.md)
+for the backend model and the servers the agent can program.
+
+> [!NOTE]
+>
+> In `single` discovery mode the replicator serves the cluster it runs in, so you
+> can point the replicator and agent at one cluster for a self-contained DNS
+> service with no separate control plane. Run the two roles as separate
+> deployments; select each with `--role`.
+
+## Control Planes and Clusters
+
+| Tier | What runs there | Trust boundary |
+|------|-----------------|----------------|
+| **Tenant / project control planes** | User-authored `DNSZone`, `DNSRecordSet`, `DNSZoneClass` | Owned by tenants; isolated per project |
+| **Platform control plane** | Registry of project control planes used for discovery | Platform-operated |
+| **Replication control plane** | DNS operator (replicator role) | Platform-operated; holds cross-cluster credentials |
+| **Authoritative cluster** | DNS operator (agent role) + DNS backend | Platform-operated; single writer of authoritative data |
+| **Serving layer** | Read-only authoritative servers | Platform-operated; internet-facing |
+
+## Discovery
+
+The replicator's `discovery.mode` (in the
+[`DNSOperator` server config](#operator-configuration)) selects how the
+replicator finds upstream control planes:
+
+- **`single`** — the operator serves exactly one upstream cluster: the cluster it
+ runs in. The operator performs no external discovery.
+- **`milo`** — the operator queries a **platform control plane** for the set of
+ project control planes and connects to each one using a connection template.
+ The operator picks up new projects as they appear.
+
+Discovery decouples the number of tenants from the operator's deployment: adding
+a tenant control plane requires no change to the DNS operator.
+
+## Authoritative Serving and State Replication
+
+The authoritative cluster holds the source of truth, but it does not necessarily
+answer public queries itself. To scale and distribute serving, a backend can
+replicate its authoritative zone data to a separate **serving layer** of
+read-only nodes that answer queries close to end users. A simpler backend may
+instead answer queries directly from the authoritative server, with no separate
+serving layer at all.
+
+Where a serving layer exists, each node holds only its own replica, so you can
+add nodes close to users, and an anycast address typically fronts them so
+resolvers reach the nearest one.
+
+**How** a backend replicates its data and serves it is backend-specific — zone
+transfers, shared storage, and clustered databases are all valid approaches. For
+the PowerDNS mechanism — LMDB snapshots shipped through LightningStream to object
+storage, with a local recursor for `ALIAS` expansion — see [PowerDNS Backend →
+Storage and Serving](./backends/powerdns.md#storage-and-serving).
+
+Each zone's `NS` and `SOA` records advertise nameserver names from the
+`DNSZoneClass` nameserver policy, and those names point at wherever the backend
+answers queries.
+
+## Supporting Components
+
+- **Service catalog** — a `Service` registration publishes DNS into the platform
+ service catalog and billing surface. See
+ [`config/components/service-catalog`](../../config/components/service-catalog/README.md).
+- **Mutating webhook** — stamps display annotations (FQDNs, record values) onto
+ record sets at admission, so downstream consumers render human-readable names
+ without re-deriving them.
+
+## Operator Configuration
+
+You configure the operator binary with a `DNSOperator` object passed via
+`--server-config`. The API does not serve this object; it configures a running
+instance. Sample:
+[`config/agent/server-config.yaml`](../../config/agent/server-config.yaml).
+
+| Field | Default | Description |
+|-------|---------|-------------|
+| `discovery.mode` | `single` | `single` (local cluster) or `milo` (discover project control planes). |
+| `discovery.internalServiceDiscovery` | `false` | Use internal service addresses when connecting to discovered control planes. |
+| `discovery.discoveryKubeconfigPath` | — | Kubeconfig for the platform control plane used for discovery. |
+| `discovery.projectKubeconfigPath` | — | Connection template for discovered project control planes. |
+| `downstreamResourceManagement.kubeconfigPath` | — | Kubeconfig for the authoritative (downstream) cluster. |
+| `downstreamResourceManagement.dnsZoneAccountingNamespace` | `datum-downstream-dnszone-accounting` | Namespace holding the zone ownership ledger. |
+
+Each backend contributes its own `controllers.` tuning and connection
+settings. For the PowerDNS controller options and environment variables, see
+[PowerDNS Backend → Configuration](./backends/powerdns.md#configuration).
+
+## Deployment Overlays
+
+The repository ships Kustomize bases and overlays for each role:
+
+| Path | Role | Notes |
+|------|------|-------|
+| [`config/overlays/agent-powerdns`](../../config/overlays/agent-powerdns) | Downstream agent | Runnable agent + PowerDNS: builds on the `config/agent` base and adds the namespace and a storage backend |
+| [`config/agent`](../../config/agent) | Downstream agent (base) | Manager, PowerDNS, and config; consumed by the overlay above rather than applied directly |
+| [`config/overlays/replicator`](../../config/overlays/replicator) | Replicator | Wires downstream cluster credentials and discovery mode |
+| [`config/milo`](../../config/milo) | Component | Installs platform integration resources into tenant control planes |
+
+See the [project README](../../README.md#deploying) for quickstart instructions.