<mxfile host="app.diagrams.net" modified="2026-04-17T12:00:00.000Z" agent="Claude" version="24.7.17">
  <diagram id="c4-container" name="C4 Container">
    <mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1700" pageHeight="1300" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <!-- Title -->
        <mxCell id="title" value="Container Diagram — Portfolio Website" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontSize=20;fontStyle=1;fontColor=#1F2937;" vertex="1" parent="1">
          <mxGeometry x="400" y="20" width="800" height="40" as="geometry"/>
        </mxCell>
        <mxCell id="subtitle" value="[C4 Level 2 — Container]" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontSize=12;fontStyle=2;fontColor=#6B7280;" vertex="1" parent="1">
          <mxGeometry x="600" y="55" width="400" height="25" as="geometry"/>
        </mxCell>

        <!-- Person: Visitor -->
        <mxCell id="visitor" value="&lt;b&gt;Visitor / Recruiter&lt;/b&gt;&lt;br&gt;&lt;br&gt;Browses portfolio,&lt;br&gt;sends contact form,&lt;br&gt;downloads CV" style="shape=mxgraph.c4.person2;whiteSpace=wrap;html=1;align=center;fillColor=#08427B;fontColor=#ffffff;strokeColor=#073B6F;fontSize=11;" vertex="1" parent="1">
          <mxGeometry x="730" y="90" width="180" height="160" as="geometry"/>
        </mxCell>

        <!-- System boundary -->
        <mxCell id="sys_boundary" value="Portfolio Website [Software System]" style="swimlane;fontStyle=1;fillColor=none;strokeColor=#438DD5;startSize=32;rounded=1;arcSize=3;fontSize=13;fontColor=#438DD5;strokeWidth=2;dashed=1;dashPattern=8 4;" vertex="1" parent="1">
          <mxGeometry x="60" y="310" width="1570" height="680" as="geometry"/>
        </mxCell>

        <!-- Container: SPA -->
        <mxCell id="spa" value="&lt;b&gt;Single-Page Application&lt;/b&gt;&lt;br&gt;[Container: React 19 + TypeScript]&lt;br&gt;&lt;br&gt;Portfolio UI: home, about, projects,&lt;br&gt;contact, CV download.&lt;br&gt;Tailwind CSS + shadcn/ui.&lt;br&gt;Built with Vite, served by nginx.&lt;br&gt;VITE_DOCS_URL baked at build time." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="50" y="55" width="280" height="175" as="geometry"/>
        </mxCell>

        <!-- Container: API -->
        <mxCell id="api" value="&lt;b&gt;API Backend&lt;/b&gt;&lt;br&gt;[Container: FastAPI + Python 3.11]&lt;br&gt;&lt;br&gt;REST API: /api/contact,&lt;br&gt;/api/download-cv, /api/projects,&lt;br&gt;/api/experience, /api/skills.&lt;br&gt;Rate limited via slowapi.&lt;br&gt;Uvicorn (port 8000)." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="520" y="55" width="280" height="175" as="geometry"/>
        </mxCell>

        <!-- Container: Docs (MkDocs) -->
        <mxCell id="docs" value="&lt;b&gt;Documentation Site&lt;/b&gt;&lt;br&gt;[Container: MkDocs Material]&lt;br&gt;&lt;br&gt;Architecture docs, ADRs,&lt;br&gt;API reference, planning docs,&lt;br&gt;cost dashboard, coverage report.&lt;br&gt;Hosted on Azure Static Web App.&lt;br&gt;Port 8080 (local docker-compose)." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="990" y="55" width="290" height="175" as="geometry"/>
        </mxCell>

        <!-- Container: nginx -->
        <mxCell id="nginx" value="&lt;b&gt;nginx Reverse Proxy&lt;/b&gt;&lt;br&gt;[Sidecar: nginx 1.25]&lt;br&gt;&lt;br&gt;Serves static frontend assets.&lt;br&gt;Proxies /api/* to backend&lt;br&gt;on port 8000." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="50" y="320" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- Container: PostgreSQL -->
        <mxCell id="postgres" value="&lt;b&gt;PostgreSQL Flexible Server&lt;/b&gt;&lt;br&gt;[Container: Azure Managed DB]&lt;br&gt;&lt;br&gt;Prod only (AAD-only auth,&lt;br&gt;Managed Identity connection).&lt;br&gt;Tables: projects, skill_categories,&lt;br&gt;work_entries." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="520" y="320" width="280" height="140" as="geometry"/>
        </mxCell>

        <!-- Container: Key Vault -->
        <mxCell id="kv" value="&lt;b&gt;Azure Key Vault&lt;/b&gt;&lt;br&gt;[Container: Managed Service]&lt;br&gt;&lt;br&gt;Stores email credentials,&lt;br&gt;Entra client secret,&lt;br&gt;DATABASE_URL, SWA token." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="800" y="320" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- Container: ACR -->
        <mxCell id="acr" value="&lt;b&gt;Azure Container Registry&lt;/b&gt;&lt;br&gt;[Container: Managed Service]&lt;br&gt;&lt;br&gt;Stores Docker images for&lt;br&gt;frontend, backend, and CMS containers." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="1130" y="320" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- Container: UIGen API Explorer -->
        <mxCell id="uigen" value="&lt;b&gt;UIGen API Explorer&lt;/b&gt;&lt;br&gt;[Container: Node 20 + @uigen-dev/cli]&lt;br&gt;&lt;br&gt;Auto-generated React UI from&lt;br&gt;the backend OpenAPI spec.&lt;br&gt;admin.sven-relijveld.com&lt;br&gt;Guest read-only + admin writes." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1168BD;fontColor=#ffffff;strokeColor=#0E5BA3;arcSize=5;fontSize=10;align=center;" vertex="1" parent="sys_boundary">
          <mxGeometry x="1290" y="55" width="240" height="155" as="geometry"/>
        </mxCell>

        <!-- Container: CMS -->
        <mxCell id="cms" value="&lt;b&gt;CMS&lt;/b&gt;&lt;br&gt;[Container: Strapi v5 + Node 22]&lt;br&gt;&lt;br&gt;Headless content management.&lt;br&gt;/admin UI for content editing.&lt;br&gt;REST API: /cms/api/*&lt;br&gt;cms.dev.sven-relijveld.com&lt;br&gt;Scales to zero (min 0 replicas)." style="fillColor=#438DD5;fontColor=#ffffff;strokeColor=#3C7FC0;arcSize=5;fontSize=10;align=center;rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="sys_boundary">
          <mxGeometry x="270" y="320" width="220" height="155" as="geometry"/>
        </mxCell>

        <!-- CMS → PostgreSQL -->
        <mxCell id="e_cms_pg" value="SQLAlchemy / Managed Identity&lt;br&gt;(strapi schema)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="cms" target="postgres" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- CMS → Key Vault -->
        <mxCell id="e_cms_kv" value="Reads Strapi secrets&lt;br&gt;(Managed Identity)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="cms" target="kv" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- SPA → CMS -->
        <mxCell id="e_spa_cms" value="Fetches content&lt;br&gt;/cms/api/* (REST)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="spa" target="cms" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- UIGen → API (proxies /api/*) -->
        <mxCell id="e_uigen_api" value="Proxies /api/* to backend&lt;br&gt;(built-in HTTP proxy)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#1168BD;strokeWidth=1.5;fontSize=9;fontColor=#1168BD;" edge="1" source="uigen" target="api" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1410" y="140"/>
              <mxPoint x="660" y="140"/>
            </Array>
          </mxGeometry>
        </mxCell>


        <!-- External: Gmail -->
        <mxCell id="gmail" value="&lt;b&gt;Gmail SMTP&lt;/b&gt;&lt;br&gt;[External System]&lt;br&gt;&lt;br&gt;Delivers notification and&lt;br&gt;confirmation emails" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="600" y="1060" width="260" height="120" as="geometry"/>
        </mxCell>

        <!-- External: Entra -->
        <mxCell id="entra" value="&lt;b&gt;Microsoft Entra ID&lt;/b&gt;&lt;br&gt;[External System]&lt;br&gt;&lt;br&gt;OIDC for CI/CD,&lt;br&gt;EasyAuth for dev" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="80" y="1060" width="220" height="120" as="geometry"/>
        </mxCell>

        <!-- External: GitHub Actions -->
        <mxCell id="ghactions" value="&lt;b&gt;GitHub Actions&lt;/b&gt;&lt;br&gt;[External System]&lt;br&gt;&lt;br&gt;CI/CD workflows,&lt;br&gt;builds images, deploys&lt;br&gt;to Azure via OIDC.&lt;br&gt;cost-report.yml (weekly)." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="1320" y="1060" width="250" height="120" as="geometry"/>
        </mxCell>

        <!-- External: Cost Management -->
        <mxCell id="cost_mgmt" value="&lt;b&gt;Azure Cost Management&lt;/b&gt;&lt;br&gt;[External System]&lt;br&gt;&lt;br&gt;Queried weekly by&lt;br&gt;cost-report workflow" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#999999;fontColor=#ffffff;strokeColor=#8C8C8C;arcSize=5;fontSize=10;align=center;" vertex="1" parent="1">
          <mxGeometry x="1090" y="1060" width="210" height="120" as="geometry"/>
        </mxCell>

        <!-- Relationships -->
        <!-- Visitor → SPA -->
        <mxCell id="e1" value="Visits via HTTPS&lt;br&gt;(www.sven-relijveld.com)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="visitor" target="spa" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="440" y="170"/>
              <mxPoint x="440" y="390"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- Visitor → Docs -->
        <mxCell id="e1b" value="Reads docs&lt;br&gt;(docs.sven-relijveld.com)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="visitor" target="docs" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1180" y="170"/>
              <mxPoint x="1180" y="380"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- SPA → nginx -->
        <mxCell id="e2" value="Served by" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="spa" target="nginx" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- nginx → API -->
        <mxCell id="e3" value="Proxies /api/*&lt;br&gt;requests to port 8000" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="nginx" target="api" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="470" y="385"/>
              <mxPoint x="470" y="145"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- API → Key Vault -->
        <mxCell id="e4" value="Reads secrets&lt;br&gt;(Managed Identity)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="api" target="kv" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- API → PostgreSQL -->
        <mxCell id="e4b" value="SQLAlchemy async&lt;br&gt;(Managed Identity)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="api" target="postgres" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- API → Gmail -->
        <mxCell id="e5" value="Sends emails&lt;br&gt;SMTP:587/TLS" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;fontSize=9;fontColor=#707070;" edge="1" source="api" target="gmail" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- ACR → nginx (image pull) -->
        <mxCell id="e6" value="Pulls images" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="acr" target="nginx" parent="sys_boundary">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1080" y="385"/>
              <mxPoint x="190" y="385"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- GitHub Actions → ACR -->
        <mxCell id="e7" value="Pushes images" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="ghactions" target="acr" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- GitHub Actions → Docs (swa deploy) -->
        <mxCell id="e7b" value="swa deploy" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="ghactions" target="docs" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1445" y="950"/>
              <mxPoint x="1270" y="950"/>
              <mxPoint x="1270" y="450"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- GitHub Actions → Entra -->
        <mxCell id="e8" value="OIDC token exchange" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="ghactions" target="entra" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1445" y="1185"/>
              <mxPoint x="190" y="1185"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- GitHub Actions → Cost Management -->
        <mxCell id="e9" value="cost-report.yml&lt;br&gt;(weekly)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#707070;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#707070;" edge="1" source="ghactions" target="cost_mgmt" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- Legend -->
        <mxCell id="legend_bg" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F9FAFB;strokeColor=#D1D5DB;arcSize=5;" vertex="1" parent="1">
          <mxGeometry x="30" y="90" width="320" height="195" as="geometry"/>
        </mxCell>
        <mxCell id="leg_title" value="Legend" style="text;html=1;strokeColor=none;fillColor=none;fontSize=11;fontStyle=1;fontColor=#374151;" vertex="1" parent="1">
          <mxGeometry x="45" y="98" width="60" height="20" as="geometry"/>
        </mxCell>
        <mxCell id="leg_p" value="" style="shape=mxgraph.c4.person2;fillColor=#08427B;fontColor=#ffffff;strokeColor=#073B6F;" vertex="1" parent="1">
          <mxGeometry x="50" y="125" width="24" height="28" as="geometry"/>
        </mxCell>
        <mxCell id="leg_p_l" value="Person" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="82" y="130" width="80" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_c" value="" style="rounded=1;fillColor=#438DD5;strokeColor=#3C7FC0;" vertex="1" parent="1">
          <mxGeometry x="50" y="160" width="24" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_c_l" value="Container" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="82" y="160" width="80" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_e" value="" style="rounded=1;fillColor=#999999;strokeColor=#8C8C8C;" vertex="1" parent="1">
          <mxGeometry x="50" y="185" width="24" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_e_l" value="External System" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="82" y="185" width="120" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b" value="" style="rounded=1;fillColor=none;strokeColor=#438DD5;dashed=1;dashPattern=8 4;strokeWidth=2;" vertex="1" parent="1">
          <mxGeometry x="50" y="210" width="24" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_b_l" value="System Boundary" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="82" y="210" width="120" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_dev" value="" style="rounded=1;fillColor=#1168BD;strokeColor=#0E5BA3;" vertex="1" parent="1">
          <mxGeometry x="50" y="235" width="24" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_dev_l" value="Container (deployed)" style="text;html=1;strokeColor=none;fillColor=none;fontSize=10;" vertex="1" parent="1">
          <mxGeometry x="82" y="235" width="120" height="18" as="geometry"/>
        </mxCell>
        <mxCell id="leg_new" value="★ Updated: CMS (Strapi v5) container added" style="text;html=1;strokeColor=none;fillColor=#FEF9C3;rounded=1;arcSize=10;fontSize=9;fontStyle=2;fontColor=#374151;" vertex="1" parent="1">
          <mxGeometry x="42" y="260" width="290" height="20" as="geometry"/>
        </mxCell>

      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
