chartlet 0.16.0

Compile structured data into deterministic, accessible static charts
Documentation
1
<div class="chartlet-wrapper chartlet-responsive chartlet-bp-640"><style>.chartlet-wrapper.chartlet-responsive{display:block;width:100%;container-type:inline-size}.chartlet-variant-mobile{display:none}@container (max-width:639px){.chartlet-bp-640 .chartlet-variant-desktop{display:none}.chartlet-bp-640 .chartlet-variant-mobile{display:block}}</style><style>.chartlet-stage{position:relative;display:inline-block;max-width:100%;vertical-align:top}.chartlet-stage>svg{display:block}.chartlet-hotspot{position:absolute;border-radius:8px;cursor:pointer}.chartlet-focus{margin:0 0 12px 0;font-size:13px}.chartlet-focus summary{cursor:pointer;min-height:24px}.chartlet-focus fieldset{border:none;padding:0;margin:4px 0 0 0}.chartlet-focus label{display:inline-flex;align-items:center;min-height:44px;margin-right:14px;cursor:pointer;white-space:nowrap}.chartlet-focus input{margin-right:4px}.chartlet-focus input:focus-visible{outline:2px solid #2563eb;outline-offset:2px}.chartlet-wrapper:has(.chartlet-focus-node:checked) #chartlet-9ef1ff81027644fa .chartlet-f{opacity:.18}#chartlet-9ef1ff81027644fa-hs-0{left:7.633%;top:4.222%;width:9%;height:5.333%}#chartlet-9ef1ff81027644fa-hs-1{left:25.956%;top:4.222%;width:10.222%;height:5.333%}#chartlet-9ef1ff81027644fa-hs-2{left:45.5%;top:4.222%;width:9%;height:5.333%}#chartlet-9ef1ff81027644fa-hs-3{left:63.6%;top:4.222%;width:10.667%;height:5.333%}#chartlet-9ef1ff81027644fa-hs-4{left:83.2%;top:4.222%;width:9.333%;height:5.333%}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa .chartlet-f-0{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa .chartlet-n-0 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa .chartlet-n-0 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa .chartlet-f-1{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa .chartlet-n-1 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa .chartlet-n-1 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa .chartlet-f-2{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa .chartlet-n-2 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa .chartlet-n-2 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa .chartlet-f-3{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa .chartlet-n-3 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa .chartlet-n-3 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa .chartlet-f-4{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa .chartlet-n-4 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa .chartlet-n-4 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(.chartlet-focus-node:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f{opacity:.18}#chartlet-9ef1ff81027644fa-m-hs-0{left:4.025%;top:4.13%;width:16.75%;height:6.848%}#chartlet-9ef1ff81027644fa-m-hs-1{left:23.325%;top:4.13%;width:15.75%;height:6.848%}#chartlet-9ef1ff81027644fa-m-hs-2{left:41.625%;top:4.13%;width:16.75%;height:6.848%}#chartlet-9ef1ff81027644fa-m-hs-3{left:61.675%;top:4.13%;width:14.25%;height:6.848%}#chartlet-9ef1ff81027644fa-m-hs-4{left:78.85%;top:4.13%;width:17.5%;height:6.848%}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f-0{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-0 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-0:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-0 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f-1{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-1 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-1:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-1 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f-2{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-2 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-2:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-2 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f-3{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-3 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-3:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-3 .chartlet-seq-box{stroke-width:3}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa-m .chartlet-f-4{opacity:1}.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-4 .chartlet-flow-node,.chartlet-wrapper:has(#chartlet-9ef1ff81027644fa-focus-4:checked) #chartlet-9ef1ff81027644fa-m .chartlet-n-4 .chartlet-seq-box{stroke-width:3}</style><details class="chartlet-focus"><summary>Focus</summary><fieldset><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" checked> Show all</label><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" class="chartlet-focus-node" id="chartlet-9ef1ff81027644fa-focus-0"> Browser</label><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" class="chartlet-focus-node" id="chartlet-9ef1ff81027644fa-focus-1"> Web / BFF</label><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" class="chartlet-focus-node" id="chartlet-9ef1ff81027644fa-focus-2"> Keycloak</label><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" class="chartlet-focus-node" id="chartlet-9ef1ff81027644fa-focus-3"> Domain API</label><label><input type="radio" name="chartlet-9ef1ff81027644fa-focus" class="chartlet-focus-node" id="chartlet-9ef1ff81027644fa-focus-4"> Postgres</label></fieldset></details><figure class="chartlet-figure" data-viz="chart"><figcaption>Sign-in with OIDC and a backend for frontend</figcaption><div class="chartlet-variant-desktop"><div class="chartlet-stage"><svg xmlns="http://www.w3.org/2000/svg" width="900" height="900" viewBox="0 0 900 900" id="chartlet-9ef1ff81027644fa" role="img" aria-labelledby="chartlet-9ef1ff81027644fa-title chartlet-9ef1ff81027644fa-description" class="chartlet-root"><title id="chartlet-9ef1ff81027644fa-title">Sign-in with OIDC and a backend for frontend</title><desc id="chartlet-9ef1ff81027644fa-description">Sequence diagram with 5 participants: Browser (actor), Web / BFF, Keycloak, Domain API and Postgres (database). 15 messages, in order: 1. Browser to Web / BFF: GET /grades. 2. Web / BFF to Browser: Redirect, PKCE (reply). 3. Browser to Keycloak: Sign in, 2FA. 4. Keycloak to Browser: Code (reply). 5. Browser to Web / BFF: Callback with code. 6. Web / BFF to Keycloak: Exchange code. 7. Keycloak to Web / BFF: Tokens (reply). 8. Web / BFF to itself: Create session. 9. Web / BFF to Domain API: Load grades. 10. Domain API to Postgres: Set tenant, SELECT. 11. Postgres to Domain API: own school only (reply). 12. Domain API to Web / BFF: Grades (reply). 13. Web / BFF to Browser: Page, session cookie (reply). 14. Keycloak to Web / BFF: Backchannel logout (asynchronous). 15. Web / BFF to itself: End sessions. Fragment opt &quot;central sign-out&quot; spans messages 14–15.</desc><style>.chartlet-root{--chartlet-text:#172033;--chartlet-muted:#344054;--chartlet-grid:#d9dee8;--chartlet-zero:#667085;--chartlet-accent:#2563eb;--chartlet-background:#fff;--chartlet-color-1:#2563eb;--chartlet-color-2:#c2410c;--chartlet-color-3:#475569;--chartlet-color-4:#111827;max-width:100%;height:auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-title{font-size:22px;font-weight:650;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-label,#chartlet-9ef1ff81027644fa .chartlet-tick,#chartlet-9ef1ff81027644fa .chartlet-value,#chartlet-9ef1ff81027644fa .chartlet-value-inverse,#chartlet-9ef1ff81027644fa .chartlet-axis-title{font-size:12px;fill:var(--chartlet-muted)}#chartlet-9ef1ff81027644fa .chartlet-value,#chartlet-9ef1ff81027644fa .chartlet-value-inverse{font-weight:600}#chartlet-9ef1ff81027644fa .chartlet-value{fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-value-inverse{fill:var(--chartlet-background)}#chartlet-9ef1ff81027644fa .chartlet-axis-title{font-weight:600}#chartlet-9ef1ff81027644fa .chartlet-grid{stroke:var(--chartlet-grid);stroke-width:1}#chartlet-9ef1ff81027644fa .chartlet-zero{stroke:var(--chartlet-zero);stroke-width:1.5}#chartlet-9ef1ff81027644fa .chartlet-bar,#chartlet-9ef1ff81027644fa .chartlet-point{fill:var(--chartlet-accent)}#chartlet-9ef1ff81027644fa .chartlet-line{fill:none;stroke:var(--chartlet-accent);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}#chartlet-9ef1ff81027644fa .chartlet-background{fill:var(--chartlet-background)}.chartlet-root{--chartlet-node:#eef3fd;--chartlet-lane:#f7f9fc;--chartlet-lane-head:#eef1f6;--chartlet-shadow:rgba(16,24,40,.09);--chartlet-role-blue:#2563eb;--chartlet-role-blue-fill:#eef3fd;--chartlet-role-green:#15803d;--chartlet-role-green-fill:#ecfdf3;--chartlet-role-amber:#b45309;--chartlet-role-amber-fill:#fff7e8;--chartlet-role-teal:#0f766e;--chartlet-role-teal-fill:#e9faf7;--chartlet-role-violet:#6d28d9;--chartlet-role-violet-fill:#f3efff;--chartlet-role-gray:#667085;--chartlet-role-gray-fill:#f7f8fa;--chartlet-role-red:#b42318;--chartlet-role-red-fill:#fef3f2}.chartlet-root.chartlet-theme-dark{--chartlet-node:#1a2436;--chartlet-lane:#121925;--chartlet-lane-head:#18212f;--chartlet-shadow:rgba(0,0,0,.45);--chartlet-role-blue:#7ea6ff;--chartlet-role-blue-fill:#1a2436;--chartlet-role-green:#4ade80;--chartlet-role-green-fill:#132a1d;--chartlet-role-amber:#fbbf24;--chartlet-role-amber-fill:#2b2210;--chartlet-role-teal:#2dd4bf;--chartlet-role-teal-fill:#0f2a27;--chartlet-role-violet:#b69cff;--chartlet-role-violet-fill:#231c38;--chartlet-role-gray:#9aa7bd;--chartlet-role-gray-fill:#161d29;--chartlet-role-red:#f97066;--chartlet-role-red-fill:#2d1616}#chartlet-9ef1ff81027644fa .chartlet-role-blue{fill:var(--chartlet-role-blue-fill);stroke:var(--chartlet-role-blue)}#chartlet-9ef1ff81027644fa .chartlet-role-green{fill:var(--chartlet-role-green-fill);stroke:var(--chartlet-role-green)}#chartlet-9ef1ff81027644fa .chartlet-role-amber{fill:var(--chartlet-role-amber-fill);stroke:var(--chartlet-role-amber)}#chartlet-9ef1ff81027644fa .chartlet-role-teal{fill:var(--chartlet-role-teal-fill);stroke:var(--chartlet-role-teal)}#chartlet-9ef1ff81027644fa .chartlet-role-violet{fill:var(--chartlet-role-violet-fill);stroke:var(--chartlet-role-violet)}#chartlet-9ef1ff81027644fa .chartlet-role-gray{fill:var(--chartlet-role-gray-fill);stroke:var(--chartlet-role-gray)}#chartlet-9ef1ff81027644fa .chartlet-role-red{fill:var(--chartlet-role-red-fill);stroke:var(--chartlet-role-red)}#chartlet-9ef1ff81027644fa .chartlet-diagram-rim{fill:none;stroke:var(--chartlet-role-teal);stroke-width:1.5}#chartlet-9ef1ff81027644fa .chartlet-diagram-shadow{fill:var(--chartlet-shadow);stroke:none;rx:7px}#chartlet-9ef1ff81027644fa .chartlet-diagram-chip{fill:var(--chartlet-background);stroke:var(--chartlet-grid);stroke-width:1;rx:4px}#chartlet-9ef1ff81027644fa .chartlet-diagram-chip-text{font-size:12px;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-diagram-badge{fill:var(--chartlet-accent);stroke:var(--chartlet-background);stroke-width:2}#chartlet-9ef1ff81027644fa .chartlet-diagram-badge-text{font-size:10px;font-weight:700;fill:var(--chartlet-background)}#chartlet-9ef1ff81027644fa .chartlet-seq-box{stroke-width:1.5;stroke-linejoin:round;rx:7px}#chartlet-9ef1ff81027644fa .chartlet-seq-external{stroke-dasharray:5 3}#chartlet-9ef1ff81027644fa .chartlet-seq-hit{fill:transparent;stroke:none}#chartlet-9ef1ff81027644fa .chartlet-seq-actor{stroke:var(--chartlet-text);stroke-width:1.5;stroke-linecap:round}#chartlet-9ef1ff81027644fa .chartlet-seq-actor-head{fill:var(--chartlet-background);stroke:var(--chartlet-text);stroke-width:1.5}#chartlet-9ef1ff81027644fa .chartlet-seq-label{font-size:13px;font-weight:600;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-seq-sublabel{font-size:11px;fill:var(--chartlet-muted)}#chartlet-9ef1ff81027644fa .chartlet-seq-lifeline{stroke:var(--chartlet-zero);stroke-width:1;stroke-dasharray:3 4}#chartlet-9ef1ff81027644fa .chartlet-seq-activation{fill:var(--chartlet-node);stroke:var(--chartlet-accent);stroke-width:1;rx:2px}#chartlet-9ef1ff81027644fa .chartlet-seq-message{fill:none;stroke:var(--chartlet-zero);stroke-width:1.4;stroke-linejoin:round}#chartlet-9ef1ff81027644fa .chartlet-seq-reply{stroke-dasharray:5 4}#chartlet-9ef1ff81027644fa .chartlet-seq-head{fill:var(--chartlet-zero);stroke:var(--chartlet-zero);stroke-width:1;stroke-linejoin:round}#chartlet-9ef1ff81027644fa .chartlet-seq-head-open{fill:none;stroke:var(--chartlet-zero);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}#chartlet-9ef1ff81027644fa .chartlet-seq-frame{fill:none;stroke:var(--chartlet-zero);stroke-width:1;rx:6px}#chartlet-9ef1ff81027644fa .chartlet-seq-tab{fill:var(--chartlet-node);stroke:var(--chartlet-zero);stroke-width:1;rx:4px}#chartlet-9ef1ff81027644fa .chartlet-seq-tag{font-size:11px;font-weight:650;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa .chartlet-seq-guard{font-size:11px;fill:var(--chartlet-muted);paint-order:stroke;stroke:var(--chartlet-background);stroke-width:3px;stroke-linejoin:round}#chartlet-9ef1ff81027644fa .chartlet-seq-branch{stroke:var(--chartlet-zero);stroke-width:1;stroke-dasharray:5 4}</style><g class="chartlet-f chartlet-n-0 chartlet-f-0 chartlet-f-1 chartlet-f-2"><line x1="109.2" y1="86" x2="109.2" y2="884" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><line x1="279.6" y1="86" x2="279.6" y2="884" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><line x1="450" y1="86" x2="450" y2="884" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><line x1="620.4" y1="86" x2="620.4" y2="884" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><line x1="790.8" y1="86" x2="790.8" y2="884" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="274.6" y="135.474" width="10" height="46.56" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="445" y="228.594" width="10" height="46.56" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="274.6" y="321.714" width="10" height="392.434" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="445" y="368.274" width="10" height="46.56" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><rect x="615.4" y="527.909" width="10" height="139.68" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><rect x="785.8" y="574.469" width="10" height="46.56" class="chartlet-seq-activation"/></g><rect x="207.6" y="725.234" width="314.4" height="143.68" class="chartlet-seq-frame"/><rect x="207.6" y="725.234" width="30.48" height="16" class="chartlet-seq-tab"/><text x="213.6" y="737.234" text-anchor="start" class="chartlet-seq-tag">opt</text><text x="244.08" y="737.234" text-anchor="start" class="chartlet-seq-guard">[central sign-out]</text><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="109.2,135.474 266.6,135.474" class="chartlet-seq-message"><title>1. Browser → Web / BFF: GET /grades</title></polyline><polyline points="266.6,138.974 274.6,135.474 266.6,131.974 266.6,138.974" class="chartlet-seq-head"/><rect x="149.4" y="105.474" width="85" height="21" class="chartlet-diagram-chip"/><text x="191.9" y="119.474" text-anchor="middle" class="chartlet-diagram-chip-text">GET /grades</text><circle cx="125.2" cy="135.474" r="8" class="chartlet-diagram-badge"/><text x="125.2" y="138.974" text-anchor="middle" class="chartlet-diagram-badge-text">1</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="274.6,182.034 117.2,182.034" class="chartlet-seq-message chartlet-seq-reply"><title>2. Web / BFF → Browser: Redirect, PKCE</title></polyline><polyline points="117.2,178.534 109.2,182.034 117.2,185.534" class="chartlet-seq-head-open"/><rect x="141.12" y="152.034" width="101.56" height="21" class="chartlet-diagram-chip"/><text x="191.9" y="166.034" text-anchor="middle" class="chartlet-diagram-chip-text">Redirect, PKCE</text><circle cx="258.6" cy="182.034" r="8" class="chartlet-diagram-badge"/><text x="258.6" y="185.534" text-anchor="middle" class="chartlet-diagram-badge-text">2</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-2"><polyline points="109.2,228.594 437,228.594" class="chartlet-seq-message"><title>3. Browser → Keycloak: Sign in, 2FA</title></polyline><polyline points="437,232.094 445,228.594 437,225.094 437,232.094" class="chartlet-seq-head"/><rect x="237.36" y="198.594" width="79.48" height="21" class="chartlet-diagram-chip"/><text x="277.1" y="212.594" text-anchor="middle" class="chartlet-diagram-chip-text">Sign in, 2FA</text><circle cx="125.2" cy="228.594" r="8" class="chartlet-diagram-badge"/><text x="125.2" y="232.094" text-anchor="middle" class="chartlet-diagram-badge-text">3</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-2"><polyline points="445,275.154 117.2,275.154" class="chartlet-seq-message chartlet-seq-reply"><title>4. Keycloak → Browser: Code</title></polyline><polyline points="117.2,271.654 109.2,275.154 117.2,278.654" class="chartlet-seq-head-open"/><rect x="258" y="245.154" width="38.2" height="21" class="chartlet-diagram-chip"/><text x="277.1" y="259.154" text-anchor="middle" class="chartlet-diagram-chip-text">Code</text><circle cx="429" cy="275.154" r="8" class="chartlet-diagram-badge"/><text x="429" y="278.654" text-anchor="middle" class="chartlet-diagram-badge-text">4</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="109.2,321.714 266.6,321.714" class="chartlet-seq-message"><title>5. Browser → Web / BFF: Callback with code</title></polyline><polyline points="266.6,325.214 274.6,321.714 266.6,318.214 266.6,325.214" class="chartlet-seq-head"/><rect x="131.28" y="291.714" width="121.24" height="21" class="chartlet-diagram-chip"/><text x="191.9" y="305.714" text-anchor="middle" class="chartlet-diagram-chip-text">Callback with code</text><circle cx="125.2" cy="321.714" r="8" class="chartlet-diagram-badge"/><text x="125.2" y="325.214" text-anchor="middle" class="chartlet-diagram-badge-text">5</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="284.6,368.274 437,368.274" class="chartlet-seq-message"><title>6. Web / BFF → Keycloak: Exchange code</title></polyline><polyline points="437,371.774 445,368.274 437,364.774 437,371.774" class="chartlet-seq-head"/><rect x="316.9" y="338.274" width="95.8" height="21" class="chartlet-diagram-chip"/><text x="364.8" y="352.274" text-anchor="middle" class="chartlet-diagram-chip-text">Exchange code</text><circle cx="300.6" cy="368.274" r="8" class="chartlet-diagram-badge"/><text x="300.6" y="371.774" text-anchor="middle" class="chartlet-diagram-badge-text">6</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="445,414.834 292.6,414.834" class="chartlet-seq-message chartlet-seq-reply"><title>7. Keycloak → Web / BFF: Tokens</title></polyline><polyline points="292.6,411.334 284.6,414.834 292.6,418.334" class="chartlet-seq-head-open"/><rect x="338.98" y="384.834" width="51.64" height="21" class="chartlet-diagram-chip"/><text x="364.8" y="398.834" text-anchor="middle" class="chartlet-diagram-chip-text">Tokens</text><circle cx="429" cy="414.834" r="8" class="chartlet-diagram-badge"/><text x="429" y="418.334" text-anchor="middle" class="chartlet-diagram-badge-text">7</text></g><g class="chartlet-f chartlet-f-1"><polyline points="284.6,461.394 302.6,461.394 305.225,461.769 307.1,462.894 308.225,464.769 308.6,467.394 308.6,471.394 308.225,474.019 307.1,475.894 305.225,477.019 302.6,477.394 292.6,477.394" class="chartlet-seq-message"><title>8. Web / BFF → Web / BFF: Create session</title></polyline><polyline points="292.6,473.894 284.6,477.394 292.6,480.894 292.6,473.894" class="chartlet-seq-head"/><rect x="286.6" y="431.394" width="99.4" height="21" class="chartlet-diagram-chip"/><text x="291.6" y="445.394" text-anchor="start" class="chartlet-diagram-chip-text">Create session</text><circle cx="308.6" cy="469.394" r="8" class="chartlet-diagram-badge"/><text x="308.6" y="472.894" text-anchor="middle" class="chartlet-diagram-badge-text">8</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-3"><polyline points="284.6,527.909 607.4,527.909" class="chartlet-seq-message"><title>9. Web / BFF → Domain API: Load grades</title></polyline><polyline points="607.4,531.409 615.4,527.909 607.4,524.409 607.4,531.409" class="chartlet-seq-head"/><rect x="408.82" y="497.909" width="82.36" height="21" class="chartlet-diagram-chip"/><text x="450" y="511.909" text-anchor="middle" class="chartlet-diagram-chip-text">Load grades</text><circle cx="300.6" cy="527.909" r="8" class="chartlet-diagram-badge"/><text x="300.6" y="531.409" text-anchor="middle" class="chartlet-diagram-badge-text">9</text></g><g class="chartlet-f chartlet-f-3 chartlet-f-4"><polyline points="625.4,574.469 777.8,574.469" class="chartlet-seq-message"><title>10. Domain API → Postgres: Set tenant, SELECT</title></polyline><polyline points="777.8,577.969 785.8,574.469 777.8,570.969 777.8,577.969" class="chartlet-seq-head"/><rect x="639.94" y="544.469" width="131.32" height="21" class="chartlet-diagram-chip"/><text x="705.6" y="558.469" text-anchor="middle" class="chartlet-diagram-chip-text">Set tenant, SELECT</text><circle cx="641.4" cy="574.469" r="8" class="chartlet-diagram-badge"/><text x="641.4" y="577.969" text-anchor="middle" class="chartlet-diagram-badge-text">10</text></g><g class="chartlet-f chartlet-f-3 chartlet-f-4"><polyline points="785.8,621.029 633.4,621.029" class="chartlet-seq-message chartlet-seq-reply"><title>11. Postgres → Domain API: own school only</title></polyline><polyline points="633.4,617.529 625.4,621.029 633.4,624.529" class="chartlet-seq-head-open"/><rect x="654.16" y="591.029" width="102.88" height="21" class="chartlet-diagram-chip"/><text x="705.6" y="605.029" text-anchor="middle" class="chartlet-diagram-chip-text">own school only</text><circle cx="769.8" cy="621.029" r="8" class="chartlet-diagram-badge"/><text x="769.8" y="624.529" text-anchor="middle" class="chartlet-diagram-badge-text">11</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-3"><polyline points="615.4,667.589 292.6,667.589" class="chartlet-seq-message chartlet-seq-reply"><title>12. Domain API → Web / BFF: Grades</title></polyline><polyline points="292.6,664.089 284.6,667.589 292.6,671.089" class="chartlet-seq-head-open"/><rect x="424.18" y="637.589" width="51.64" height="21" class="chartlet-diagram-chip"/><text x="450" y="651.589" text-anchor="middle" class="chartlet-diagram-chip-text">Grades</text><circle cx="599.4" cy="667.589" r="8" class="chartlet-diagram-badge"/><text x="599.4" y="671.089" text-anchor="middle" class="chartlet-diagram-badge-text">12</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="274.6,714.149 117.2,714.149" class="chartlet-seq-message chartlet-seq-reply"><title>13. Web / BFF → Browser: Page, session cookie</title></polyline><polyline points="117.2,710.649 109.2,714.149 117.2,717.649" class="chartlet-seq-head-open"/><rect x="126.6" y="684.149" width="130.6" height="21" class="chartlet-diagram-chip"/><text x="191.9" y="698.149" text-anchor="middle" class="chartlet-diagram-chip-text">Page, session cookie</text><circle cx="258.6" cy="714.149" r="8" class="chartlet-diagram-badge"/><text x="258.6" y="717.649" text-anchor="middle" class="chartlet-diagram-badge-text">13</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="450,787.314 287.6,787.314" class="chartlet-seq-message"><title>14. Keycloak → Web / BFF: Backchannel logout</title></polyline><polyline points="287.6,783.814 279.6,787.314 287.6,790.814" class="chartlet-seq-head-open"/><rect x="303.22" y="757.314" width="123.16" height="21" class="chartlet-diagram-chip"/><text x="364.8" y="771.314" text-anchor="middle" class="chartlet-diagram-chip-text">Backchannel logout</text><circle cx="434" cy="787.314" r="8" class="chartlet-diagram-badge"/><text x="434" y="790.814" text-anchor="middle" class="chartlet-diagram-badge-text">14</text></g><g class="chartlet-f chartlet-f-1"><polyline points="279.6,833.874 297.6,833.874 300.225,834.249 302.1,835.374 303.225,837.249 303.6,839.874 303.6,843.874 303.225,846.499 302.1,848.374 300.225,849.499 297.6,849.874 287.6,849.874" class="chartlet-seq-message"><title>15. Web / BFF → Web / BFF: End sessions</title></polyline><polyline points="287.6,846.374 279.6,849.874 287.6,853.374 287.6,846.374" class="chartlet-seq-head"/><rect x="281.6" y="803.874" width="85.96" height="21" class="chartlet-diagram-chip"/><text x="286.6" y="817.874" text-anchor="start" class="chartlet-diagram-chip-text">End sessions</text><circle cx="303.6" cy="841.874" r="8" class="chartlet-diagram-badge"/><text x="303.6" y="845.374" text-anchor="middle" class="chartlet-diagram-badge-text">15</text></g><g class="chartlet-f chartlet-n-0 chartlet-f-0 chartlet-f-1 chartlet-f-2"><circle cx="109.2" cy="17.5" r="4.5" class="chartlet-seq-actor-head"/><line x1="109.2" y1="22" x2="109.2" y2="29" class="chartlet-seq-actor"/><line x1="102.2" y1="24.5" x2="116.2" y2="24.5" class="chartlet-seq-actor"/><line x1="109.2" y1="29" x2="103.2" y2="35" class="chartlet-seq-actor"/><line x1="109.2" y1="29" x2="115.2" y2="35" class="chartlet-seq-actor"/><rect x="68.7" y="38" width="81" height="48" class="chartlet-seq-hit"><title>Browser</title></rect><text x="109.2" y="66.5" text-anchor="middle" class="chartlet-seq-label">Browser</text></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="233.6" y="40.5" width="92" height="48" class="chartlet-diagram-shadow"/><rect x="233.6" y="38" width="92" height="48" class="chartlet-seq-box chartlet-role-blue"><title>Web / BFF – SvelteKit</title></rect><text x="279.6" y="60" text-anchor="middle" class="chartlet-seq-label">Web / BFF</text><text x="279.6" y="75" text-anchor="middle" class="chartlet-seq-sublabel">SvelteKit</text></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="409.5" y="40.5" width="81" height="48" class="chartlet-diagram-shadow"/><rect x="409.5" y="38" width="81" height="48" class="chartlet-seq-box chartlet-role-blue"><title>Keycloak – OIDC</title></rect><text x="450" y="60" text-anchor="middle" class="chartlet-seq-label">Keycloak</text><text x="450" y="75" text-anchor="middle" class="chartlet-seq-sublabel">OIDC</text></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><rect x="572.4" y="40.5" width="96" height="48" class="chartlet-diagram-shadow"/><rect x="572.4" y="38" width="96" height="48" class="chartlet-seq-box chartlet-role-blue"><title>Domain API – Rust</title></rect><text x="620.4" y="60" text-anchor="middle" class="chartlet-seq-label">Domain API</text><text x="620.4" y="75" text-anchor="middle" class="chartlet-seq-sublabel">Rust</text></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><polyline points="748.8,45.5 748.8,83.5 750.231,84.794 754.427,86 761.102,87.036 769.8,87.83 779.93,88.33 790.8,88.5 801.67,88.33 811.8,87.83 820.498,87.036 827.173,86 831.369,84.794 832.8,83.5 832.8,45.5 831.369,44.206 827.173,43 820.498,41.964 811.8,41.17 801.67,40.67 790.8,40.5 779.93,40.67 769.8,41.17 761.102,41.964 754.427,43 750.231,44.206 748.8,45.5 748.8,45.5" class="chartlet-diagram-shadow"/><polyline points="748.8,43 748.8,81 750.231,82.294 754.427,83.5 761.102,84.536 769.8,85.33 779.93,85.83 790.8,86 801.67,85.83 811.8,85.33 820.498,84.536 827.173,83.5 831.369,82.294 832.8,81 832.8,43 831.369,41.706 827.173,40.5 820.498,39.464 811.8,38.67 801.67,38.17 790.8,38 779.93,38.17 769.8,38.67 761.102,39.464 754.427,40.5 750.231,41.706 748.8,43 748.8,43" class="chartlet-seq-box chartlet-role-teal"><title>Postgres – RLS</title></polyline><polyline points="748.8,43 750.231,44.294 754.427,45.5 761.102,46.536 769.8,47.33 779.93,47.83 790.8,48 801.67,47.83 811.8,47.33 820.498,46.536 827.173,45.5 831.369,44.294 832.8,43" class="chartlet-diagram-rim"/><text x="790.8" y="60" text-anchor="middle" class="chartlet-seq-label">Postgres</text><text x="790.8" y="75" text-anchor="middle" class="chartlet-seq-sublabel">RLS</text></g></svg><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-hs-0" for="chartlet-9ef1ff81027644fa-focus-0"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-hs-1" for="chartlet-9ef1ff81027644fa-focus-1"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-hs-2" for="chartlet-9ef1ff81027644fa-focus-2"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-hs-3" for="chartlet-9ef1ff81027644fa-focus-3"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-hs-4" for="chartlet-9ef1ff81027644fa-focus-4"></label></div></div><div class="chartlet-variant-mobile"><div class="chartlet-stage"><svg xmlns="http://www.w3.org/2000/svg" width="400" height="920" viewBox="0 0 400 920" id="chartlet-9ef1ff81027644fa-m" role="img" aria-labelledby="chartlet-9ef1ff81027644fa-m-title chartlet-9ef1ff81027644fa-m-description" class="chartlet-root"><title id="chartlet-9ef1ff81027644fa-m-title">Sign-in with OIDC and a backend for frontend</title><desc id="chartlet-9ef1ff81027644fa-m-description">Sequence diagram with 5 participants: Browser (actor), Web / BFF, Keycloak, Domain API and Postgres (database). 15 messages, in order: 1. Browser to Web / BFF: GET /grades. 2. Web / BFF to Browser: Redirect, PKCE (reply). 3. Browser to Keycloak: Sign in, 2FA. 4. Keycloak to Browser: Code (reply). 5. Browser to Web / BFF: Callback with code. 6. Web / BFF to Keycloak: Exchange code. 7. Keycloak to Web / BFF: Tokens (reply). 8. Web / BFF to itself: Create session. 9. Web / BFF to Domain API: Load grades. 10. Domain API to Postgres: Set tenant, SELECT. 11. Postgres to Domain API: own school only (reply). 12. Domain API to Web / BFF: Grades (reply). 13. Web / BFF to Browser: Page, session cookie (reply). 14. Keycloak to Web / BFF: Backchannel logout (asynchronous). 15. Web / BFF to itself: End sessions. Fragment opt &quot;central sign-out&quot; spans messages 14–15.</desc><style>.chartlet-root{--chartlet-text:#172033;--chartlet-muted:#344054;--chartlet-grid:#d9dee8;--chartlet-zero:#667085;--chartlet-accent:#2563eb;--chartlet-background:#fff;--chartlet-color-1:#2563eb;--chartlet-color-2:#c2410c;--chartlet-color-3:#475569;--chartlet-color-4:#111827;max-width:100%;height:auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-title{font-size:22px;font-weight:650;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-label,#chartlet-9ef1ff81027644fa-m .chartlet-tick,#chartlet-9ef1ff81027644fa-m .chartlet-value,#chartlet-9ef1ff81027644fa-m .chartlet-value-inverse,#chartlet-9ef1ff81027644fa-m .chartlet-axis-title{font-size:12px;fill:var(--chartlet-muted)}#chartlet-9ef1ff81027644fa-m .chartlet-value,#chartlet-9ef1ff81027644fa-m .chartlet-value-inverse{font-weight:600}#chartlet-9ef1ff81027644fa-m .chartlet-value{fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-value-inverse{fill:var(--chartlet-background)}#chartlet-9ef1ff81027644fa-m .chartlet-axis-title{font-weight:600}#chartlet-9ef1ff81027644fa-m .chartlet-grid{stroke:var(--chartlet-grid);stroke-width:1}#chartlet-9ef1ff81027644fa-m .chartlet-zero{stroke:var(--chartlet-zero);stroke-width:1.5}#chartlet-9ef1ff81027644fa-m .chartlet-bar,#chartlet-9ef1ff81027644fa-m .chartlet-point{fill:var(--chartlet-accent)}#chartlet-9ef1ff81027644fa-m .chartlet-line{fill:none;stroke:var(--chartlet-accent);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}#chartlet-9ef1ff81027644fa-m .chartlet-background{fill:var(--chartlet-background)}#chartlet-9ef1ff81027644fa-m .chartlet-title-small{font-size:18px}.chartlet-root{--chartlet-node:#eef3fd;--chartlet-lane:#f7f9fc;--chartlet-lane-head:#eef1f6;--chartlet-shadow:rgba(16,24,40,.09);--chartlet-role-blue:#2563eb;--chartlet-role-blue-fill:#eef3fd;--chartlet-role-green:#15803d;--chartlet-role-green-fill:#ecfdf3;--chartlet-role-amber:#b45309;--chartlet-role-amber-fill:#fff7e8;--chartlet-role-teal:#0f766e;--chartlet-role-teal-fill:#e9faf7;--chartlet-role-violet:#6d28d9;--chartlet-role-violet-fill:#f3efff;--chartlet-role-gray:#667085;--chartlet-role-gray-fill:#f7f8fa;--chartlet-role-red:#b42318;--chartlet-role-red-fill:#fef3f2}.chartlet-root.chartlet-theme-dark{--chartlet-node:#1a2436;--chartlet-lane:#121925;--chartlet-lane-head:#18212f;--chartlet-shadow:rgba(0,0,0,.45);--chartlet-role-blue:#7ea6ff;--chartlet-role-blue-fill:#1a2436;--chartlet-role-green:#4ade80;--chartlet-role-green-fill:#132a1d;--chartlet-role-amber:#fbbf24;--chartlet-role-amber-fill:#2b2210;--chartlet-role-teal:#2dd4bf;--chartlet-role-teal-fill:#0f2a27;--chartlet-role-violet:#b69cff;--chartlet-role-violet-fill:#231c38;--chartlet-role-gray:#9aa7bd;--chartlet-role-gray-fill:#161d29;--chartlet-role-red:#f97066;--chartlet-role-red-fill:#2d1616}#chartlet-9ef1ff81027644fa-m .chartlet-role-blue{fill:var(--chartlet-role-blue-fill);stroke:var(--chartlet-role-blue)}#chartlet-9ef1ff81027644fa-m .chartlet-role-green{fill:var(--chartlet-role-green-fill);stroke:var(--chartlet-role-green)}#chartlet-9ef1ff81027644fa-m .chartlet-role-amber{fill:var(--chartlet-role-amber-fill);stroke:var(--chartlet-role-amber)}#chartlet-9ef1ff81027644fa-m .chartlet-role-teal{fill:var(--chartlet-role-teal-fill);stroke:var(--chartlet-role-teal)}#chartlet-9ef1ff81027644fa-m .chartlet-role-violet{fill:var(--chartlet-role-violet-fill);stroke:var(--chartlet-role-violet)}#chartlet-9ef1ff81027644fa-m .chartlet-role-gray{fill:var(--chartlet-role-gray-fill);stroke:var(--chartlet-role-gray)}#chartlet-9ef1ff81027644fa-m .chartlet-role-red{fill:var(--chartlet-role-red-fill);stroke:var(--chartlet-role-red)}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-rim{fill:none;stroke:var(--chartlet-role-teal);stroke-width:1.5}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-shadow{fill:var(--chartlet-shadow);stroke:none;rx:7px}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-chip{fill:var(--chartlet-background);stroke:var(--chartlet-grid);stroke-width:1;rx:4px}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-chip-text{font-size:12px;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-badge{fill:var(--chartlet-accent);stroke:var(--chartlet-background);stroke-width:2}#chartlet-9ef1ff81027644fa-m .chartlet-diagram-badge-text{font-size:10px;font-weight:700;fill:var(--chartlet-background)}#chartlet-9ef1ff81027644fa-m .chartlet-seq-box{stroke-width:1.5;stroke-linejoin:round;rx:7px}#chartlet-9ef1ff81027644fa-m .chartlet-seq-external{stroke-dasharray:5 3}#chartlet-9ef1ff81027644fa-m .chartlet-seq-hit{fill:transparent;stroke:none}#chartlet-9ef1ff81027644fa-m .chartlet-seq-actor{stroke:var(--chartlet-text);stroke-width:1.5;stroke-linecap:round}#chartlet-9ef1ff81027644fa-m .chartlet-seq-actor-head{fill:var(--chartlet-background);stroke:var(--chartlet-text);stroke-width:1.5}#chartlet-9ef1ff81027644fa-m .chartlet-seq-label{font-size:13px;font-weight:600;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-seq-sublabel{font-size:11px;fill:var(--chartlet-muted)}#chartlet-9ef1ff81027644fa-m .chartlet-seq-lifeline{stroke:var(--chartlet-zero);stroke-width:1;stroke-dasharray:3 4}#chartlet-9ef1ff81027644fa-m .chartlet-seq-activation{fill:var(--chartlet-node);stroke:var(--chartlet-accent);stroke-width:1;rx:2px}#chartlet-9ef1ff81027644fa-m .chartlet-seq-message{fill:none;stroke:var(--chartlet-zero);stroke-width:1.4;stroke-linejoin:round}#chartlet-9ef1ff81027644fa-m .chartlet-seq-reply{stroke-dasharray:5 4}#chartlet-9ef1ff81027644fa-m .chartlet-seq-head{fill:var(--chartlet-zero);stroke:var(--chartlet-zero);stroke-width:1;stroke-linejoin:round}#chartlet-9ef1ff81027644fa-m .chartlet-seq-head-open{fill:none;stroke:var(--chartlet-zero);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}#chartlet-9ef1ff81027644fa-m .chartlet-seq-frame{fill:none;stroke:var(--chartlet-zero);stroke-width:1;rx:6px}#chartlet-9ef1ff81027644fa-m .chartlet-seq-tab{fill:var(--chartlet-node);stroke:var(--chartlet-zero);stroke-width:1;rx:4px}#chartlet-9ef1ff81027644fa-m .chartlet-seq-tag{font-size:11px;font-weight:650;fill:var(--chartlet-text)}#chartlet-9ef1ff81027644fa-m .chartlet-seq-guard{font-size:11px;fill:var(--chartlet-muted);paint-order:stroke;stroke:var(--chartlet-background);stroke-width:3px;stroke-linejoin:round}#chartlet-9ef1ff81027644fa-m .chartlet-seq-branch{stroke:var(--chartlet-zero);stroke-width:1;stroke-dasharray:5 4}</style><g class="chartlet-f chartlet-n-0 chartlet-f-0 chartlet-f-1 chartlet-f-2"><line x1="49.6" y1="101" x2="49.6" y2="904" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><line x1="124.8" y1="101" x2="124.8" y2="904" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><line x1="200" y1="101" x2="200" y2="904" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><line x1="275.2" y1="101" x2="275.2" y2="904" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><line x1="350.4" y1="101" x2="350.4" y2="904" class="chartlet-seq-lifeline"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="119.8" y="150.703" width="10" height="46.86" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="195" y="244.423" width="10" height="46.86" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="119.8" y="338.143" width="10" height="394.963" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="195" y="385.003" width="10" height="46.86" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><rect x="270.2" y="545.666" width="10" height="140.58" class="chartlet-seq-activation"/></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><rect x="345.4" y="592.526" width="10" height="46.86" class="chartlet-seq-activation"/></g><rect x="91.2" y="744.263" width="145" height="144.58" class="chartlet-seq-frame"/><rect x="91.2" y="744.263" width="30.48" height="16" class="chartlet-seq-tab"/><text x="97.2" y="756.263" text-anchor="start" class="chartlet-seq-tag">opt</text><text x="127.68" y="756.263" text-anchor="start" class="chartlet-seq-guard">[central sign-out]</text><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="49.6,150.703 111.8,150.703" class="chartlet-seq-message"><title>1. Browser → Web / BFF: GET /grades</title></polyline><polyline points="111.8,154.203 119.8,150.703 111.8,147.203 111.8,154.203" class="chartlet-seq-head"/><rect x="42.2" y="120.703" width="85" height="21" class="chartlet-diagram-chip"/><text x="84.7" y="134.703" text-anchor="middle" class="chartlet-diagram-chip-text">GET /grades</text><circle cx="65.6" cy="150.703" r="8" class="chartlet-diagram-badge"/><text x="65.6" y="154.203" text-anchor="middle" class="chartlet-diagram-badge-text">1</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="119.8,197.563 57.6,197.563" class="chartlet-seq-message chartlet-seq-reply"><title>2. Web / BFF → Browser: Redirect, PKCE</title></polyline><polyline points="57.6,194.063 49.6,197.563 57.6,201.063" class="chartlet-seq-head-open"/><rect x="33.92" y="167.563" width="101.56" height="21" class="chartlet-diagram-chip"/><text x="84.7" y="181.563" text-anchor="middle" class="chartlet-diagram-chip-text">Redirect, PKCE</text><circle cx="103.8" cy="197.563" r="8" class="chartlet-diagram-badge"/><text x="103.8" y="201.063" text-anchor="middle" class="chartlet-diagram-badge-text">2</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-2"><polyline points="49.6,244.423 187,244.423" class="chartlet-seq-message"><title>3. Browser → Keycloak: Sign in, 2FA</title></polyline><polyline points="187,247.923 195,244.423 187,240.923 187,247.923" class="chartlet-seq-head"/><rect x="82.56" y="214.423" width="79.48" height="21" class="chartlet-diagram-chip"/><text x="122.3" y="228.423" text-anchor="middle" class="chartlet-diagram-chip-text">Sign in, 2FA</text><circle cx="65.6" cy="244.423" r="8" class="chartlet-diagram-badge"/><text x="65.6" y="247.923" text-anchor="middle" class="chartlet-diagram-badge-text">3</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-2"><polyline points="195,291.283 57.6,291.283" class="chartlet-seq-message chartlet-seq-reply"><title>4. Keycloak → Browser: Code</title></polyline><polyline points="57.6,287.783 49.6,291.283 57.6,294.783" class="chartlet-seq-head-open"/><rect x="103.2" y="261.283" width="38.2" height="21" class="chartlet-diagram-chip"/><text x="122.3" y="275.283" text-anchor="middle" class="chartlet-diagram-chip-text">Code</text><circle cx="179" cy="291.283" r="8" class="chartlet-diagram-badge"/><text x="179" y="294.783" text-anchor="middle" class="chartlet-diagram-badge-text">4</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="49.6,338.143 111.8,338.143" class="chartlet-seq-message"><title>5. Browser → Web / BFF: Callback with code</title></polyline><polyline points="111.8,341.643 119.8,338.143 111.8,334.643 111.8,341.643" class="chartlet-seq-head"/><rect x="24.08" y="308.143" width="121.24" height="21" class="chartlet-diagram-chip"/><text x="84.7" y="322.143" text-anchor="middle" class="chartlet-diagram-chip-text">Callback with code</text><circle cx="65.6" cy="338.143" r="8" class="chartlet-diagram-badge"/><text x="65.6" y="341.643" text-anchor="middle" class="chartlet-diagram-badge-text">5</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="129.8,385.003 187,385.003" class="chartlet-seq-message"><title>6. Web / BFF → Keycloak: Exchange code</title></polyline><polyline points="187,388.503 195,385.003 187,381.503 187,388.503" class="chartlet-seq-head"/><rect x="114.5" y="355.003" width="95.8" height="21" class="chartlet-diagram-chip"/><text x="162.4" y="369.003" text-anchor="middle" class="chartlet-diagram-chip-text">Exchange code</text><circle cx="145.8" cy="385.003" r="8" class="chartlet-diagram-badge"/><text x="145.8" y="388.503" text-anchor="middle" class="chartlet-diagram-badge-text">6</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="195,431.863 137.8,431.863" class="chartlet-seq-message chartlet-seq-reply"><title>7. Keycloak → Web / BFF: Tokens</title></polyline><polyline points="137.8,428.363 129.8,431.863 137.8,435.363" class="chartlet-seq-head-open"/><rect x="136.58" y="401.863" width="51.64" height="21" class="chartlet-diagram-chip"/><text x="162.4" y="415.863" text-anchor="middle" class="chartlet-diagram-chip-text">Tokens</text><circle cx="179" cy="431.863" r="8" class="chartlet-diagram-badge"/><text x="179" y="435.363" text-anchor="middle" class="chartlet-diagram-badge-text">7</text></g><g class="chartlet-f chartlet-f-1"><polyline points="129.8,478.723 147.8,478.723 150.425,479.098 152.3,480.223 153.425,482.098 153.8,484.723 153.8,488.723 153.425,491.348 152.3,493.223 150.425,494.348 147.8,494.723 137.8,494.723" class="chartlet-seq-message"><title>8. Web / BFF → Web / BFF: Create session</title></polyline><polyline points="137.8,491.223 129.8,494.723 137.8,498.223 137.8,491.223" class="chartlet-seq-head"/><rect x="131.8" y="448.723" width="99.4" height="21" class="chartlet-diagram-chip"/><text x="136.8" y="462.723" text-anchor="start" class="chartlet-diagram-chip-text">Create session</text><circle cx="153.8" cy="486.723" r="8" class="chartlet-diagram-badge"/><text x="153.8" y="490.223" text-anchor="middle" class="chartlet-diagram-badge-text">8</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-3"><polyline points="129.8,545.666 262.2,545.666" class="chartlet-seq-message"><title>9. Web / BFF → Domain API: Load grades</title></polyline><polyline points="262.2,549.166 270.2,545.666 262.2,542.166 262.2,549.166" class="chartlet-seq-head"/><rect x="158.82" y="515.666" width="82.36" height="21" class="chartlet-diagram-chip"/><text x="200" y="529.666" text-anchor="middle" class="chartlet-diagram-chip-text">Load grades</text><circle cx="145.8" cy="545.666" r="8" class="chartlet-diagram-badge"/><text x="145.8" y="549.166" text-anchor="middle" class="chartlet-diagram-badge-text">9</text></g><g class="chartlet-f chartlet-f-3 chartlet-f-4"><polyline points="280.2,592.526 337.4,592.526" class="chartlet-seq-message"><title>10. Domain API → Postgres: Set tenant, SELECT</title></polyline><polyline points="337.4,596.026 345.4,592.526 337.4,589.026 337.4,596.026" class="chartlet-seq-head"/><rect x="247.14" y="562.526" width="131.32" height="21" class="chartlet-diagram-chip"/><text x="312.8" y="576.526" text-anchor="middle" class="chartlet-diagram-chip-text">Set tenant, SELECT</text><circle cx="296.2" cy="592.526" r="8" class="chartlet-diagram-badge"/><text x="296.2" y="596.026" text-anchor="middle" class="chartlet-diagram-badge-text">10</text></g><g class="chartlet-f chartlet-f-3 chartlet-f-4"><polyline points="345.4,639.386 288.2,639.386" class="chartlet-seq-message chartlet-seq-reply"><title>11. Postgres → Domain API: own school only</title></polyline><polyline points="288.2,635.886 280.2,639.386 288.2,642.886" class="chartlet-seq-head-open"/><rect x="261.36" y="609.386" width="102.88" height="21" class="chartlet-diagram-chip"/><text x="312.8" y="623.386" text-anchor="middle" class="chartlet-diagram-chip-text">own school only</text><circle cx="329.4" cy="639.386" r="8" class="chartlet-diagram-badge"/><text x="329.4" y="642.886" text-anchor="middle" class="chartlet-diagram-badge-text">11</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-3"><polyline points="270.2,686.246 137.8,686.246" class="chartlet-seq-message chartlet-seq-reply"><title>12. Domain API → Web / BFF: Grades</title></polyline><polyline points="137.8,682.746 129.8,686.246 137.8,689.746" class="chartlet-seq-head-open"/><rect x="174.18" y="656.246" width="51.64" height="21" class="chartlet-diagram-chip"/><text x="200" y="670.246" text-anchor="middle" class="chartlet-diagram-chip-text">Grades</text><circle cx="254.2" cy="686.246" r="8" class="chartlet-diagram-badge"/><text x="254.2" y="689.746" text-anchor="middle" class="chartlet-diagram-badge-text">12</text></g><g class="chartlet-f chartlet-f-0 chartlet-f-1"><polyline points="119.8,733.106 57.6,733.106" class="chartlet-seq-message chartlet-seq-reply"><title>13. Web / BFF → Browser: Page, session cookie</title></polyline><polyline points="57.6,729.606 49.6,733.106 57.6,736.606" class="chartlet-seq-head-open"/><rect x="19.4" y="703.106" width="130.6" height="21" class="chartlet-diagram-chip"/><text x="84.7" y="717.106" text-anchor="middle" class="chartlet-diagram-chip-text">Page, session cookie</text><circle cx="103.8" cy="733.106" r="8" class="chartlet-diagram-badge"/><text x="103.8" y="736.606" text-anchor="middle" class="chartlet-diagram-badge-text">13</text></g><g class="chartlet-f chartlet-f-1 chartlet-f-2"><polyline points="200,806.743 132.8,806.743" class="chartlet-seq-message"><title>14. Keycloak → Web / BFF: Backchannel logout</title></polyline><polyline points="132.8,803.243 124.8,806.743 132.8,810.243" class="chartlet-seq-head-open"/><rect x="100.82" y="776.743" width="123.16" height="21" class="chartlet-diagram-chip"/><text x="162.4" y="790.743" text-anchor="middle" class="chartlet-diagram-chip-text">Backchannel logout</text><circle cx="184" cy="806.743" r="8" class="chartlet-diagram-badge"/><text x="184" y="810.243" text-anchor="middle" class="chartlet-diagram-badge-text">14</text></g><g class="chartlet-f chartlet-f-1"><polyline points="124.8,853.603 142.8,853.603 145.425,853.978 147.3,855.103 148.425,856.978 148.8,859.603 148.8,863.603 148.425,866.228 147.3,868.103 145.425,869.228 142.8,869.603 132.8,869.603" class="chartlet-seq-message"><title>15. Web / BFF → Web / BFF: End sessions</title></polyline><polyline points="132.8,866.103 124.8,869.603 132.8,873.103 132.8,866.103" class="chartlet-seq-head"/><rect x="126.8" y="823.603" width="85.96" height="21" class="chartlet-diagram-chip"/><text x="131.8" y="837.603" text-anchor="start" class="chartlet-diagram-chip-text">End sessions</text><circle cx="148.8" cy="861.603" r="8" class="chartlet-diagram-badge"/><text x="148.8" y="865.103" text-anchor="middle" class="chartlet-diagram-badge-text">15</text></g><g class="chartlet-f chartlet-n-0 chartlet-f-0 chartlet-f-1 chartlet-f-2"><circle cx="49.6" cy="17.5" r="4.5" class="chartlet-seq-actor-head"/><line x1="49.6" y1="22" x2="49.6" y2="29" class="chartlet-seq-actor"/><line x1="42.6" y1="24.5" x2="56.6" y2="24.5" class="chartlet-seq-actor"/><line x1="49.6" y1="29" x2="43.6" y2="35" class="chartlet-seq-actor"/><line x1="49.6" y1="29" x2="55.6" y2="35" class="chartlet-seq-actor"/><rect x="16.1" y="38" width="67" height="63" class="chartlet-seq-hit"><title>Browser</title></rect><text x="49.6" y="74" text-anchor="middle" class="chartlet-seq-label">Browser</text></g><g class="chartlet-f chartlet-n-1 chartlet-f-0 chartlet-f-1 chartlet-f-2 chartlet-f-3"><rect x="93.3" y="40.5" width="63" height="63" class="chartlet-diagram-shadow"/><rect x="93.3" y="38" width="63" height="63" class="chartlet-seq-box chartlet-role-blue"><title>Web / BFF – SvelteKit</title></rect><text x="124.8" y="60" text-anchor="middle" class="chartlet-seq-label">Web /</text><text x="124.8" y="75" text-anchor="middle" class="chartlet-seq-label">BFF</text><text x="124.8" y="90" text-anchor="middle" class="chartlet-seq-sublabel">SvelteKit</text></g><g class="chartlet-f chartlet-n-2 chartlet-f-0 chartlet-f-1 chartlet-f-2"><rect x="166.5" y="40.5" width="67" height="63" class="chartlet-diagram-shadow"/><rect x="166.5" y="38" width="67" height="63" class="chartlet-seq-box chartlet-role-blue"><title>Keycloak – OIDC</title></rect><text x="200" y="67.5" text-anchor="middle" class="chartlet-seq-label">Keycloak</text><text x="200" y="82.5" text-anchor="middle" class="chartlet-seq-sublabel">OIDC</text></g><g class="chartlet-f chartlet-n-3 chartlet-f-1 chartlet-f-3 chartlet-f-4"><rect x="246.7" y="40.5" width="57" height="63" class="chartlet-diagram-shadow"/><rect x="246.7" y="38" width="57" height="63" class="chartlet-seq-box chartlet-role-blue"><title>Domain API – Rust</title></rect><text x="275.2" y="60" text-anchor="middle" class="chartlet-seq-label">Domain</text><text x="275.2" y="75" text-anchor="middle" class="chartlet-seq-label">API</text><text x="275.2" y="90" text-anchor="middle" class="chartlet-seq-sublabel">Rust</text></g><g class="chartlet-f chartlet-n-4 chartlet-f-3 chartlet-f-4"><polyline points="315.4,45.5 315.4,98.5 316.593,99.794 320.089,101 325.651,102.036 332.9,102.83 341.341,103.33 350.4,103.5 359.459,103.33 367.9,102.83 375.149,102.036 380.711,101 384.207,99.794 385.4,98.5 385.4,45.5 384.207,44.206 380.711,43 375.149,41.964 367.9,41.17 359.459,40.67 350.4,40.5 341.341,40.67 332.9,41.17 325.651,41.964 320.089,43 316.593,44.206 315.4,45.5 315.4,45.5" class="chartlet-diagram-shadow"/><polyline points="315.4,43 315.4,96 316.593,97.294 320.089,98.5 325.651,99.536 332.9,100.33 341.341,100.83 350.4,101 359.459,100.83 367.9,100.33 375.149,99.536 380.711,98.5 384.207,97.294 385.4,96 385.4,43 384.207,41.706 380.711,40.5 375.149,39.464 367.9,38.67 359.459,38.17 350.4,38 341.341,38.17 332.9,38.67 325.651,39.464 320.089,40.5 316.593,41.706 315.4,43 315.4,43" class="chartlet-seq-box chartlet-role-teal"><title>Postgres – RLS</title></polyline><polyline points="315.4,43 316.593,44.294 320.089,45.5 325.651,46.536 332.9,47.33 341.341,47.83 350.4,48 359.459,47.83 367.9,47.33 375.149,46.536 380.711,45.5 384.207,44.294 385.4,43" class="chartlet-diagram-rim"/><text x="350.4" y="67.5" text-anchor="middle" class="chartlet-seq-label">Postgres</text><text x="350.4" y="82.5" text-anchor="middle" class="chartlet-seq-sublabel">RLS</text></g></svg><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-m-hs-0" for="chartlet-9ef1ff81027644fa-focus-0"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-m-hs-1" for="chartlet-9ef1ff81027644fa-focus-1"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-m-hs-2" for="chartlet-9ef1ff81027644fa-focus-2"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-m-hs-3" for="chartlet-9ef1ff81027644fa-focus-3"></label><label class="chartlet-hotspot" id="chartlet-9ef1ff81027644fa-m-hs-4" for="chartlet-9ef1ff81027644fa-focus-4"></label></div></div><p class="chartlet-source">Source: Illustrative reference architecture</p><details class="chartlet-data" data-viz-text><summary>Show chart data</summary><table><caption>Data for Sign-in with OIDC and a backend for frontend</caption><thead><tr><th scope="col">No.</th><th scope="col">From</th><th scope="col">To</th><th scope="col">Message</th><th scope="col">Kind</th><th scope="col">Fragment</th></tr></thead><tbody><tr><th scope="row">1</th><td>Browser</td><td>Web / BFF</td><td>GET /grades</td><td>call</td><td></td></tr><tr><th scope="row">2</th><td>Web / BFF</td><td>Browser</td><td>Redirect, PKCE</td><td>reply</td><td></td></tr><tr><th scope="row">3</th><td>Browser</td><td>Keycloak</td><td>Sign in, 2FA</td><td>call</td><td></td></tr><tr><th scope="row">4</th><td>Keycloak</td><td>Browser</td><td>Code</td><td>reply</td><td></td></tr><tr><th scope="row">5</th><td>Browser</td><td>Web / BFF</td><td>Callback with code</td><td>call</td><td></td></tr><tr><th scope="row">6</th><td>Web / BFF</td><td>Keycloak</td><td>Exchange code</td><td>call</td><td></td></tr><tr><th scope="row">7</th><td>Keycloak</td><td>Web / BFF</td><td>Tokens</td><td>reply</td><td></td></tr><tr><th scope="row">8</th><td>Web / BFF</td><td>Web / BFF</td><td>Create session</td><td>call</td><td></td></tr><tr><th scope="row">9</th><td>Web / BFF</td><td>Domain API</td><td>Load grades</td><td>call</td><td></td></tr><tr><th scope="row">10</th><td>Domain API</td><td>Postgres</td><td>Set tenant, SELECT</td><td>call</td><td></td></tr><tr><th scope="row">11</th><td>Postgres</td><td>Domain API</td><td>own school only</td><td>reply</td><td></td></tr><tr><th scope="row">12</th><td>Domain API</td><td>Web / BFF</td><td>Grades</td><td>reply</td><td></td></tr><tr><th scope="row">13</th><td>Web / BFF</td><td>Browser</td><td>Page, session cookie</td><td>reply</td><td></td></tr><tr><th scope="row">14</th><td>Keycloak</td><td>Web / BFF</td><td>Backchannel logout</td><td>asynchronous</td><td>opt [central sign-out]</td></tr><tr><th scope="row">15</th><td>Web / BFF</td><td>Web / BFF</td><td>End sessions</td><td>call</td><td>opt [central sign-out]</td></tr></tbody></table></details></figure></div>