<mxfile host="app.diagrams.net" modified="2026-06-02T00:00:00.000Z" agent="Claude" version="24.7.17">
  <diagram id="deployment" name="Deployment Diagram">
    <mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2000" pageHeight="1400" math="0" shadow="0">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>

        <!-- Title -->
        <mxCell id="title" value="Deployment 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="1000" height="40" as="geometry"/>
        </mxCell>
        <mxCell id="subtitle" value="[UML Deployment — Production Environment]" 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="500" y="55" width="800" height="25" as="geometry"/>
        </mxCell>

        <!-- AZURE CLOUD NODE -->
        <mxCell id="azure_node" value="&amp;lt;&amp;lt;cloud&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Microsoft Azure&lt;/b&gt;&lt;br&gt;West Europe" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#DAE8FC;strokeColor=#6C8EBF;fontSize=11;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="80" y="100" width="1840" height="1060" as="geometry"/>
        </mxCell>

        <!-- Container Apps Environment Node -->
        <mxCell id="cae_node" value="&amp;lt;&amp;lt;execution environment&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Container Apps Environment&lt;/b&gt;&lt;br&gt;Azure Container Apps (Consumption)" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;fontSize=10;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="120" y="200" width="1420" height="480" as="geometry"/>
        </mxCell>

        <!-- Frontend Container Node -->
        <mxCell id="fe_node" value="&amp;lt;&amp;lt;container&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;dna-{env}-portfolio-we-ca-fe&lt;/b&gt;&lt;br&gt;1–5 replicas, auto-scale at 100 req" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#D5E8D4;strokeColor=#82B366;fontSize=10;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="150" y="310" width="390" height="330" as="geometry"/>
        </mxCell>

        <!-- nginx artifact -->
        <mxCell id="nginx_art" value="&amp;lt;&amp;lt;artifact&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;nginx:1.25-alpine&lt;/b&gt;&lt;br&gt;&lt;br&gt;Serves static React build&lt;br&gt;from /usr/share/nginx/html&lt;br&gt;&lt;br&gt;Reverse proxies /api/* to&lt;br&gt;backend:8000 (BACKEND_URL)" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#82B366;fontSize=9;align=center;size=14;" vertex="1" parent="1">
          <mxGeometry x="175" y="400" width="200" height="200" as="geometry"/>
        </mxCell>

        <!-- React build artifact -->
        <mxCell id="react_art" value="&amp;lt;&amp;lt;artifact&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;React SPA Bundle&lt;/b&gt;&lt;br&gt;&lt;br&gt;Vite production build&lt;br&gt;TypeScript + Tailwind CSS&lt;br&gt;shadcn/ui components&lt;br&gt;VITE_DOCS_URL baked in" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#82B366;fontSize=9;align=center;size=14;" vertex="1" parent="1">
          <mxGeometry x="395" y="400" width="130" height="175" as="geometry"/>
        </mxCell>

        <!-- Backend Container Node -->
        <mxCell id="be_node" value="&amp;lt;&amp;lt;container&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;dna-{env}-portfolio-we-ca-be&lt;/b&gt;&lt;br&gt;1–5 replicas" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#D5E8D4;strokeColor=#82B366;fontSize=10;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="590" y="310" width="460" height="330" as="geometry"/>
        </mxCell>

        <!-- FastAPI artifact -->
        <mxCell id="fastapi_art" value="&amp;lt;&amp;lt;artifact&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;python:3.11-slim&lt;/b&gt;&lt;br&gt;&lt;br&gt;FastAPI + Uvicorn&lt;br&gt;Port 8000&lt;br&gt;&lt;br&gt;Alembic migrations at startup&lt;br&gt;Endpoints: /api/contact,&lt;br&gt;/api/download-cv, /api/projects,&lt;br&gt;/api/skills, /api/experience&lt;br&gt;&lt;br&gt;Rate limiting via slowapi" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#82B366;fontSize=9;align=center;size=14;" vertex="1" parent="1">
          <mxGeometry x="630" y="390" width="200" height="220" as="geometry"/>
        </mxCell>

        <!-- CMS Container Node (NEW) -->
        <mxCell id="cms_node" value="&amp;lt;&amp;lt;container&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;dna-{env}-portfolio-we-ca-cms&lt;/b&gt;&lt;br&gt;0–1 replicas" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#D5E8D4;strokeColor=#82B366;fontSize=10;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1110" y="310" width="390" height="330" as="geometry"/>
        </mxCell>

        <!-- Strapi artifact (NEW) -->
        <mxCell id="strapi_art" value="&amp;lt;&amp;lt;artifact&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;node:22-alpine&lt;/b&gt;&lt;br&gt;&lt;br&gt;Strapi v5 CMS&lt;br&gt;strapi start (production mode)&lt;br&gt;Port 1337&lt;br&gt;&lt;br&gt;Volume mount:&lt;br&gt;/app/cms/public/uploads&lt;br&gt;(Azure Files)" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#82B366;fontSize=9;align=center;size=14;" vertex="1" parent="1">
          <mxGeometry x="1140" y="390" width="190" height="210" as="geometry"/>
        </mxCell>

        <!-- Managed Identity (shared) -->
        <mxCell id="mi_label" value="Managed Identities (fe-identity, be-identity, cms-identity)&lt;br&gt;RBAC: Key Vault Secrets User + ACR Pull + PostgreSQL AAD" style="text;html=1;strokeColor=#D6B656;fillColor=#FEF3C7;rounded=1;arcSize=10;fontSize=9;fontStyle=2;align=center;" vertex="1" parent="1">
          <mxGeometry x="330" y="255" width="700" height="40" as="geometry"/>
        </mxCell>

        <!-- Custom Domain -->
        <mxCell id="domain_label" value="www.sven-relijveld.com (Managed TLS cert)" style="text;html=1;strokeColor=#82B366;fillColor=#D5E8D4;rounded=1;arcSize=10;fontSize=10;fontStyle=1;align=center;" vertex="1" parent="1">
          <mxGeometry x="150" y="270" width="360" height="28" as="geometry"/>
        </mxCell>

        <!-- ACR Node -->
        <mxCell id="acr_node" value="&amp;lt;&amp;lt;registry&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Azure Container Registry&lt;/b&gt;&lt;br&gt;dnaprodportfoliowesteuropeacr&lt;br&gt;&lt;br&gt;portfolio-frontend:&lt;sha&gt;&lt;br&gt;portfolio-backend:&lt;sha&gt;&lt;br&gt;portfolio-cms:&lt;sha&gt;" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#DAE8FC;strokeColor=#6C8EBF;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="120" y="820" width="280" height="160" as="geometry"/>
        </mxCell>

        <!-- Key Vault Node (UPDATED — includes Strapi secrets) -->
        <mxCell id="kv_node" value="&amp;lt;&amp;lt;vault&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Azure Key Vault&lt;/b&gt;&lt;br&gt;dna-prod-portfolio-we-kv&lt;br&gt;&lt;br&gt;email-address, email-password,&lt;br&gt;database-url, entra-client-secret,&lt;br&gt;swa-api-token, api-admin-token,&lt;br&gt;strapi-app-keys,&lt;br&gt;strapi-api-token-salt,&lt;br&gt;strapi-admin-jwt-secret,&lt;br&gt;strapi-jwt-secret" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="430" y="820" width="280" height="200" as="geometry"/>
        </mxCell>

        <!-- PostgreSQL Node -->
        <mxCell id="psql_node" value="&amp;lt;&amp;lt;database&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;PostgreSQL Flexible Server&lt;/b&gt;&lt;br&gt;dna-prod-portfolio-we-psql-fs&lt;br&gt;&lt;br&gt;AAD-only authentication&lt;br&gt;Managed Identity connection&lt;br&gt;Tables: projects, skill_categories,&lt;br&gt;work_entries&lt;br&gt;Schema: strapi" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#DBEAFE;strokeColor=#6C8EBF;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="740" y="820" width="280" height="180" as="geometry"/>
        </mxCell>

        <!-- Azure Files Node (NEW) -->
        <mxCell id="azfiles_node" value="&amp;lt;&amp;lt;storage&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Azure Files&lt;/b&gt;&lt;br&gt;CMS Uploads Share&lt;br&gt;&lt;br&gt;Mounted at:&lt;br&gt;/app/cms/public/uploads&lt;br&gt;SMB volume mount on CMS CA" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#388E3C;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1060" y="820" width="280" height="165" as="geometry"/>
        </mxCell>

        <!-- Log Analytics / App Insights -->
        <mxCell id="log_node" value="&amp;lt;&amp;lt;monitoring&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Log Analytics + App Insights&lt;/b&gt;&lt;br&gt;&lt;br&gt;Metrics, traces, logs&lt;br&gt;Performance monitoring" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#E1D5E7;strokeColor=#9673A6;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1380" y="820" width="280" height="140" as="geometry"/>
        </mxCell>

        <!-- Static Web App Node -->
        <mxCell id="swa_node" value="&amp;lt;&amp;lt;static hosting&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Azure Static Web App&lt;/b&gt;&lt;br&gt;docs.sven-relijveld.com&lt;br&gt;&lt;br&gt;MkDocs Material documentation&lt;br&gt;Architecture, API ref, planning&lt;br&gt;Cost dashboard (Plotly)&lt;br&gt;Coverage report (auto-committed)" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#E1D5E7;strokeColor=#9673A6;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1600" y="200" width="280" height="190" as="geometry"/>
        </mxCell>

        <!-- DNS Zone -->
        <mxCell id="dns_node" value="&amp;lt;&amp;lt;DNS&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Azure DNS Zone&lt;/b&gt;&lt;br&gt;sven-relijveld.com&lt;br&gt;&lt;br&gt;A, CNAME, TXT records&lt;br&gt;Managed TLS certificates" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#DAE8FC;strokeColor=#6C8EBF;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1600" y="445" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- GitHub Actions -->
        <mxCell id="gh_node" value="&amp;lt;&amp;lt;CI/CD&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;GitHub Actions&lt;/b&gt;&lt;br&gt;&lt;br&gt;OIDC → Azure (passwordless)&lt;br&gt;test.yml, deploy-*.yml&lt;br&gt;cost-report.yml, generate-cv.yml" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#F8CECC;strokeColor=#B85450;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1600" y="630" width="280" height="130" as="geometry"/>
        </mxCell>

        <!-- Gmail -->
        <mxCell id="gmail_node" value="&amp;lt;&amp;lt;external&amp;gt;&amp;gt;&lt;br&gt;&lt;b&gt;Gmail SMTP&lt;/b&gt;&lt;br&gt;smtp.gmail.com:587 (TLS)" style="shape=mxgraph.uml25.node;whiteSpace=wrap;html=1;fillColor=#F8CECC;strokeColor=#B85450;fontSize=9;align=center;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
          <mxGeometry x="1600" y="820" width="280" height="80" as="geometry"/>
        </mxCell>

        <!-- CONNECTORS -->
        <!-- nginx → backend (proxy) -->
        <mxCell id="e_proxy" value="/api/* proxy" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#374151;strokeWidth=2;fontSize=9;fontStyle=1;fontColor=#374151;" edge="1" source="nginx_art" target="fastapi_art" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- ACR → FE (pull) -->
        <mxCell id="e_acr_fe" value="docker pull" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#6C8EBF;" edge="1" source="acr_node" target="fe_node" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- ACR → BE (pull) -->
        <mxCell id="e_acr_be" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1.5;dashed=1;fontSize=9;" edge="1" source="acr_node" target="be_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="260" y="790"/>
              <mxPoint x="820" y="790"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- ACR → CMS (pull) (NEW) -->
        <mxCell id="e_acr_cms" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1.5;dashed=1;fontSize=9;" edge="1" source="acr_node" target="cms_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="260" y="800"/>
              <mxPoint x="1305" y="800"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- Backend → KV (secrets) -->
        <mxCell id="e_be_kv" value="secrets (MI)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#D6B656;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#B8860B;" edge="1" source="be_node" target="kv_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="820" y="760"/>
              <mxPoint x="570" y="760"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- Backend → PostgreSQL -->
        <mxCell id="e_be_psql" value="asyncpg (MI auth)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#3B82F6;strokeWidth=1.5;fontSize=9;fontColor=#1D4ED8;" edge="1" source="be_node" target="psql_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="820" y="775"/>
              <mxPoint x="880" y="775"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- CMS → PostgreSQL (reads strapi schema) (NEW) -->
        <mxCell id="e_cms_psql" value="strapi schema (MI auth)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#3B82F6;strokeWidth=1.5;fontSize=9;fontColor=#1D4ED8;" edge="1" source="cms_node" target="psql_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1305" y="760"/>
              <mxPoint x="880" y="760"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- CMS → Key Vault (reads strapi secrets) (NEW) -->
        <mxCell id="e_cms_kv" value="strapi secrets (MI)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#D6B656;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#B8860B;" edge="1" source="cms_node" target="kv_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1305" y="750"/>
              <mxPoint x="1305" y="745"/>
              <mxPoint x="570" y="745"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- CMS → Azure Files (uploads volume mount) (NEW) -->
        <mxCell id="e_cms_azfiles" value="SMB mount (uploads)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#388E3C;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#388E3C;" edge="1" source="cms_node" target="azfiles_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1305" y="780"/>
              <mxPoint x="1200" y="780"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- Backend → Gmail -->
        <mxCell id="e_be_gmail" value="SMTP:587" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#B85450;strokeWidth=1.5;fontSize=9;fontColor=#B85450;" edge="1" source="be_node" target="gmail_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1050" y="530"/>
              <mxPoint x="1570" y="530"/>
              <mxPoint x="1570" y="860"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- GH Actions → ACR -->
        <mxCell id="e_gh_acr" value="docker push" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#6C8EBF;" edge="1" source="gh_node" target="acr_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1580" y="700"/>
              <mxPoint x="1580" y="900"/>
              <mxPoint x="400" y="900"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- GH Actions → SWA -->
        <mxCell id="e_gh_swa" value="swa deploy" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#9673A6;strokeWidth=1.5;dashed=1;fontSize=9;fontColor=#9673A6;" edge="1" source="gh_node" target="swa_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1740" y="630"/>
              <mxPoint x="1740" y="390"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- All → Log Analytics -->
        <mxCell id="e_logs" value="telemetry" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#9673A6;strokeWidth=1;dashed=1;fontSize=8;fontColor=#9673A6;" edge="1" source="cae_node" target="log_node" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>

        <!-- DNS → CAE -->
        <mxCell id="e_dns_fe" value="A record →" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1;dashed=1;fontSize=8;fontColor=#6C8EBF;" edge="1" source="dns_node" target="fe_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1580" y="510"/>
              <mxPoint x="1580" y="480"/>
              <mxPoint x="540" y="480"/>
            </Array>
          </mxGeometry>
        </mxCell>

        <!-- DNS → SWA -->
        <mxCell id="e_dns_swa" value="CNAME →" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#6C8EBF;strokeWidth=1;dashed=1;fontSize=8;fontColor=#6C8EBF;" edge="1" source="dns_node" target="swa_node" parent="1">
          <mxGeometry relative="1" as="geometry">
            <Array as="points">
              <mxPoint x="1740" y="445"/>
              <mxPoint x="1740" y="390"/>
            </Array>
          </mxGeometry>
        </mxCell>

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