# ADR-0016: EU data residency on Cloudflare and Neon

The repository's architectural decision, rationale and consequences.

# ADR-0016: EU data residency on Cloudflare and Neon

## Status [#status]

Accepted — ticket 02.

## Context [#context]

Wundklare processes patient data under the German MDR and DSGVO. Section §1.6
pins the Neon Postgres region to `aws-eu-central-1` and the R2 bucket
`wundklare-storage` to the `eu` jurisdiction (`.eu.r2.cloudflarestorage.com`).
The Worker runtime does not have a residency guarantee by default: a Worker
runs on the nearest healthy colo, and `placement.region = aws:eu-central-1`
is a latency hint, not a residency control.

## Decision [#decision]

- Data at rest lives in the EU. Neon is pinned to `aws-eu-central-1`; the
  R2 bucket is created in the `eu` jurisdiction with the endpoint
  `<account>.eu.r2.cloudflarestorage.com`. Backups and snapshots stay in the
  same region.
- The Worker gets `placement = { region: "aws:eu-central-1" }` as a latency
  hint so most invocations stay near the origin. We do not claim this as a
  residency control.
- The honest trust boundary is client-side envelope encryption
  (ADR-0017): objects and sensitive columns are AES-GCM ciphertext before
  they leave the device, so Cloudflare's edge TLS termination and any
  non-EU Worker invocation only ever see ciphertext for clinical bytes.
  Share links are the one zero-knowledge path (ADR-0018). Server-side
  decryption only runs in logged system jobs (ARD ADR-06).
- A DPIA follow-up (owned by the service, not this ticket) names Cloudflare
  and Neon as EU-resident sub-processors and documents the edge TLS
  decryption as a residual exposure limited to ciphertext.

## Operator check (TBD) [#operator-check-tbd]

Confirm the R2 bucket is in the EU jurisdiction before the first production
deploy:

```sh
wrangler r2 bucket info wundklare-storage
```

The output must include an `eu` jurisdiction and an endpoint ending in
`.eu.r2.cloudflarestorage.com`. If it does not, re-create the bucket with
`wrangler r2 bucket create wundklare-storage --jurisdiction eu`; buckets
cannot be moved after creation.

## Consequences [#consequences]

- Choosing `eu` for R2 means the bucket cannot be accessed from the global
  endpoint; every S3-compatible client (including local MinIO fallbacks)
  reads the jurisdiction-scoped endpoint from `STORAGE_ENDPOINT`.
- Worker placement is best-effort: observability may show invocations
  outside the EU for brief periods (fleet health). Because every clinical
  byte is ciphertext at rest and in transit, this stays within the DPIA.
- A later move to a KMS (TBD) replaces the Worker-held `WUNDKLAR_KEK` with
  a service that enforces regional key access and audit logs; ADR-0017 is
  the migration surface.

