arc42 §7 — Deployment View
Status: Draft v0.1 · Date: 2026-07-20
Traces to: ADR-0001 (hosting), ADR-0003 (Kubernetes), CON-13 (residency), QAS-SCL-01, QAS-OPS-01
Companion: DevOps deep-dive — container internals, CI/CD, hardening
7.1 Infrastructure Overview#
flowchart TB
subgraph INTERNET["🌐 Internet"]
MOB["📱 Mobile clients"]
WEB["🌐 Web / Admin"]
VERIFY["🔍 Public certificate<br/>verification"]
end
subgraph KSA["🇸🇦 Kingdom of Saudi Arabia — CON-13"]
subgraph PRIMARY["Primary Region — Riyadh"]
subgraph EDGE["Edge"]
CDN["CDN"]
WAF["WAF + DDoS"]
LB["Load Balancer"]
end
subgraph K8S["Kubernetes Cluster (managed control plane)"]
subgraph NP1["Node pool: general"]
API["API pods<br/><i>HPA: RPS + p95</i>"]
WRK["Worker pods<br/><i>HPA: queue depth</i>"]
SCH["Scheduler<br/><i>singleton</i>"]
end
subgraph NP2["Node pool: memory"]
PDF["Document renderer"]
SRCH["Search engine"]
AV["Malware scanner"]
end
subgraph NP3["Node pool: media — conditional, ADR-0005"]
MEDIA["Media servers<br/><i>hostNetwork</i>"]
TURN["TURN"]
end
end
subgraph DATA["Managed data services"]
PG[("PostgreSQL<br/>primary + replica<br/>RLS enabled")]
RD[("Redis<br/>cache · queues · locks")]
OBJ[("Object Storage<br/>encrypted")]
end
end
subgraph DR["DR Region — Jeddah"]
PGDR[("PostgreSQL<br/>standby")]
OBJDR[("Object Storage<br/>replica")]
end
end
MOB & WEB --> CDN --> WAF --> LB --> API
VERIFY --> WAF
MOB -.->|"WebRTC — bypasses ingress"| MEDIA
MOB -.->|"signed URL — direct"| OBJ
API --> PG & RD & OBJ & SRCH
WRK --> PG & RD & OBJ & SRCH
WRK --> PDF & AV
SCH --> RD
PG ==>|"async replication<br/><b>never leaves KSA</b>"| PGDR
OBJ ==>|"replication"| OBJDR
style KSA fill:#e8f5e9,stroke:#2d8659
style DR fill:#f0f0f0,stroke:#888
style NP3 fill:#fff4e5,stroke:#d98c1f
The DR region is the decisive property of ADR-0001. Oracle is the only viable provider with two GA in-Kingdom regions, so disaster recovery is achievable without a single byte leaving Saudi Arabia. Every alternative forces DR to be either cross-border — reintroducing the entire transfer-compliance workstream — or same-region multi-AZ, which does not protect against regional loss.
7.2 Environments#
| Environment | Purpose | Sizing | Data |
|---|---|---|---|
| Local | Development | Docker Compose | Seeded synthetic |
| CI | Automated tests | Ephemeral containers | Fixtures |
| Staging | Pre-production verification | Minimal cluster, same topology | Synthetic only — never production personal data |
| Production | Live | Full, autoscaled | Live |
⚠️ Staging must never hold production personal data. Copying a production database to a lower environment is one of the most common PDPL breaches in practice — staging has weaker access controls, more people with access, and no audit trail. If realistic volumes are needed, generate them.
Environment parity rule: staging runs the same manifests with different replica counts and resource requests. Divergence in topology means staging stops predicting production behaviour, which is its only purpose.
7.3 Node Pools & Placement#
| Pool | Workloads | Sizing driver | Notes |
|---|---|---|---|
general |
API, workers, scheduler | CPU/memory balanced | Autoscaled; the elastic majority |
memory |
Document renderer, search, malware scanner | Memory | Renderer consumes hundreds of MB per render; search holds its index in RAM; AV holds a ~1 GB signature DB |
media (conditional) |
Media servers, TURN | Network | hostNetwork → one media pod per node. The scaling unit is a node, with minutes of boot latency. |
Placement constraints#
| Constraint | Reason |
|---|---|
| API pods spread across availability zones | Zone failure must not take the API down |
| Scheduler: exactly one replica | Duplicate scheduled jobs would double-send notifications and double-charge subscriptions |
| Media pods: one per node, anti-affinity | Host port conflict |
| Search: persistent volume, pinned | Stateful |
| Workers: split by job duration | So a 60-second notification deploy doesn't wait on a 600-second PDF job |
7.4 Data Tier#
PostgreSQL#
| Aspect | Design |
|---|---|
| Topology | Primary + synchronous standby (same region), async standby (DR region) |
| RLS | Enabled with FORCE ROW LEVEL SECURITY on all tenant-scoped tables (ADR-0007) |
| Application role | Least privilege — no UPDATE/DELETE on the audit ledger, no BYPASSRLS |
| Connection pooling | ⚠️ Transaction-mode pooling interacts dangerously with RLS session variables — see ADR-0007. Use transaction-scoped settings and reset in terminating middleware. |
| Scale path | Read replicas for reporting projections → partitioning of high-volume tables. Documented triggers, not reactive. |
| Backups | Continuous WAL archiving + periodic snapshots, encrypted, retained in-Kingdom |
Redis#
Cache, queues, locks, and rate-limit counters. Not a durable store — anything that must survive Redis loss lives in PostgreSQL. Queue jobs are the notable exception: their loss is tolerable because the originating transaction has already committed (see pipeline rules).
Object Storage#
| Bucket | Contents | Access |
|---|---|---|
quarantine/ |
Unscanned uploads | No read access from the application or admin UI |
documents/ |
Approved legal documents | Signed URL, TTL ≤ 5 min, post-authorization only |
templates/ |
Template library | Signed URL, CDN-fronted |
certificates/ |
Generated PDFs | Signed URL, owner + verifier |
public-assets/ |
Static | CDN, public |
All encrypted at rest. Legal documents additionally application-layer envelope-encrypted (Security §4).
7.5 Network Topology#
flowchart TB
subgraph PUB["Public subnet"]
LB2["Load balancer"]
NAT["NAT gateway"]
end
subgraph PRIV2["Private subnet — no inbound internet"]
NODES["Kubernetes nodes"]
end
subgraph DATA2["Data subnet — no internet route at all"]
DB2[("PostgreSQL")]
RD2[("Redis")]
end
INT["🌐"] --> LB2 --> NODES
NODES --> NAT --> INT
NODES --> DB2 & RD2
DB2 -.->|"❌ no route"| INT
style DATA2 fill:#e8f5e9,stroke:#2d8659
| Rule | Detail |
|---|---|
| Data subnet has no internet route | Not "blocked by firewall" — no route exists |
| Nodes in private subnets | Egress via NAT only |
| Default-deny NetworkPolicy, ingress and egress, per namespace | With a DNS carve-out on UDP and TCP 53 — forgetting TCP is the classic cause of "everything broke" |
| Egress allowlist to payment/video/SMS providers | ⚠️ Standard NetworkPolicy takes CIDRs, not DNS names, and providers rotate anycast IPs. Requires CNI-native FQDN policy or an egress proxy — choose the CNI deliberately rather than accepting a default. |
| Media tier: separate path | UDP, hostNetwork, no ingress, no mesh. Its own firewall rules. |
7.6 Disaster Recovery#
Objectives — proposed, requiring sponsor confirmation#
| Scenario | RTO | RPO |
|---|---|---|
| Single pod / node failure | 0 (automatic) | 0 |
| Availability zone failure | < 5 min | 0 |
| Regional failure | < 4 hours | < 15 min |
| Accidental data deletion | < 4 hours | < 15 min (point-in-time restore) |
⚠️ No RTO or RPO is stated in any source document. These are architect proposals derived from
QAS-AVL-02(99.5 % business-hours availability) and the nature of the workload — scheduled appointments and document downloads, not real-time payments. They need explicit sponsor sign-off, because they drive standby topology and therefore cost.
Regional failover#
Manual, deliberately. Automatic cross-region failover risks split-brain for a system whose availability target does not justify it. The procedure — promote the Jeddah standby, repoint DNS, verify object-storage replication lag — must be documented and rehearsed, not theoretical.
What is not covered by replication#
| Risk | Mitigation |
|---|---|
| Logical corruption (a bad migration replicates instantly) | Point-in-time recovery, not just standby promotion |
| Accidental deletion | PITR + soft-delete on high-value entities |
| Ransomware / credential compromise | Immutable, separately-credentialed backups |
| Search index loss | Rebuildable from PostgreSQL — the index is never authoritative |
The last row is a deliberate architectural property: because the search index is a projection and post-filtered against the database (Flow 5), losing it entirely is a performance incident, not a data-loss incident.
7.7 Capacity — Launch Sizing#
Order-of-magnitude only. Real figures require the provider quotes still outstanding in ADR-0001.
| Component | Launch | Growth trigger |
|---|---|---|
| API pods | 3 (min), autoscale to 10 | p95 > 300 ms sustained |
| Workers | 2 per queue class | Queue depth / oldest-job age |
| Renderer | 1, scale-to-zero when idle | Render queue depth |
| Search | 1 node | Index size / query latency |
| PostgreSQL | Modest, primary + standby | Connection saturation → replicas |
| Redis | Small | Memory pressure |
| Media | 0 at launch | Booked concurrency (predictive) |
Two properties worth exploiting#
1. Video demand is known in advance. Consultations are booked, so QAS-SCL-02 capacity is scheduled, not stochastic. The scheduler should pre-warm media capacity from the appointment calendar, with reactive HPA as a safety net only. This matters because media scale-up is node-granular and slow — reacting three minutes into a paid consultation is a refund.
2. Load concentrates in KSA business hours. Sun–Thu, 08:00–20:00 AST. Off-peak capacity should scale down aggressively (QAS-OPS-02); assume a peak:mean ratio of at least 5:1.
7.8 Open Items#
| Item | Blocks |
|---|---|
OCI me-riyadh-1 service matrix — managed Redis, registry, secrets availability unverified |
Determines self-managed vs managed for several components (ADR-0001 gap 1) |
| Provider quotes | The entire cost model |
OQ-08 recording |
Whether the media node pool exists, and its sizing |
| ADR-0005 provider choice | Whether media node pool, TURN, and hostNetwork exist at all |
| CNI selection | Whether FQDN egress policy is available (§7.5) |
| RTO/RPO sign-off | Standby topology and cost |