Architecture العربية

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 hostNetworkone 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
Tech Lead · Amir Haroun Draft v0.1 · research & design only