Skip to content

Instantly share code, notes, and snippets.

@comalex
Last active May 27, 2026 06:51
Show Gist options
  • Select an option

  • Save comalex/9a5ae3c0dfeb29ec0068ce7ad4b6ea7c to your computer and use it in GitHub Desktop.

Select an option

Save comalex/9a5ae3c0dfeb29ec0068ce7ad4b6ea7c to your computer and use it in GitHub Desktop.
Childfree Legacy — Aurora Migration: DevOps Deep Architecture Review (code-validated)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Childfree Legacy — Aurora Migration: Complete Plan + DevOps Deep Review</title>
<style>
:root{
--ink:#1a2238;--ink-soft:#4a5170;--muted:#6b7194;
--rule:#e4e6ef;--rule-soft:#f1f2f7;--bg:#fafbfc;--card:#fff;
--accent:#5b3aa6;--accent-soft:#efe9fb;
--accent-2:#0b6b6b;--accent-2-soft:#e3f3f3;
--crit:#b3261e;--crit-soft:#fdecea;
--high:#b5650a;--high-soft:#fcefe0;
--med:#8a6d00;--med-soft:#fff7d6;
--ok:#1f6f43;--ok-soft:#e6f4ec;
--code-bg:#f6f7fb;--code-ink:#2d3a87;
}
*{box-sizing:border-box;}
html{-webkit-font-smoothing:antialiased;scroll-behavior:smooth;}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;
font-size:15.5px;line-height:1.62;}
.layout{display:grid;grid-template-columns:300px 1fr;min-height:100vh;}
aside.nav{position:sticky;top:0;height:100vh;overflow-y:auto;background:var(--card);
border-right:1px solid var(--rule);padding:20px 16px;font-size:12.5px;}
aside.nav h4{text-transform:uppercase;letter-spacing:.12em;font-size:10.5px;color:var(--accent);
margin:14px 0 6px;font-weight:700;}
aside.nav h4:first-of-type{margin-top:0;}
aside.nav h4.devops{color:var(--accent-2);}
aside.nav ol{list-style:none;padding:0;margin:0;}
aside.nav a{display:block;padding:3px 7px;color:var(--ink-soft);text-decoration:none;
border-radius:4px;line-height:1.3;}
aside.nav a:hover{background:var(--accent-soft);color:var(--accent);}
aside.nav .brand{font-weight:700;color:var(--ink);font-size:13px;padding-bottom:10px;
border-bottom:1px solid var(--rule);margin-bottom:12px;}
main.content{max-width:1080px;margin:0;padding:38px 50px 96px;}
.eyebrow{text-transform:uppercase;letter-spacing:.18em;font-size:11.5px;color:var(--accent);
font-weight:700;margin-bottom:10px;}
h1{font-size:30px;line-height:1.18;margin:0 0 14px;letter-spacing:-.01em;}
.lede{font-size:16px;color:var(--ink-soft);margin:0 0 16px;max-width:880px;}
.meta{display:flex;flex-wrap:wrap;gap:8px;font-size:12.5px;}
.meta-chip{background:var(--accent-soft);color:var(--accent);padding:3px 10px;
border-radius:999px;font-weight:600;}
h2{font-size:23px;margin:48px 0 14px;padding-bottom:9px;border-bottom:1px solid var(--rule);
letter-spacing:-.005em;scroll-margin-top:18px;color:var(--accent);}
h2.devops{color:var(--accent-2);}
h3{font-size:17.5px;margin:30px 0 11px;scroll-margin-top:18px;}
h4{font-size:14.5px;margin:22px 0 6px;}
p{margin:0 0 12px;}
strong{color:var(--ink);}
em{color:var(--ink-soft);}
code{font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:.86em;
background:var(--code-bg);color:var(--code-ink);padding:1px 5px;border-radius:4px;
border:1px solid var(--rule-soft);}
pre{font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;background:var(--code-bg);
color:var(--ink);padding:14px 16px;border-radius:7px;border:1px solid var(--rule);
font-size:12px;line-height:1.5;overflow-x:auto;margin:0 0 14px;}
pre code{background:transparent;border:0;padding:0;color:inherit;font-size:inherit;}
pre.sql{background:#f6f8ef;border-color:#dde6c6;}
pre.ts{background:#f4f6ff;border-color:#d6dcf3;}
pre.sh{background:#1a2238;color:#f1f2f7;border-color:#1a2238;}
pre.sh code{color:inherit;}
pre.yaml{background:#fffbf0;border-color:#f0d58a;}
pre.diagram{background:#1a2238;color:#e8eaf2;border-color:#1a2238;}
pre.diagram code{color:inherit;}
.card{background:var(--card);border:1px solid var(--rule);border-radius:10px;
padding:16px 20px;margin:14px 0;}
.summary-card{background:var(--card);border:1px solid var(--rule);border-left:4px solid var(--accent);
border-radius:7px;padding:16px 20px;margin:20px 0 28px;}
.summary-card.crit{border-left-color:var(--crit);}
.summary-card.ok{border-left-color:var(--ok);}
.summary-card h4{margin-top:0;color:var(--accent);font-size:11px;text-transform:uppercase;
letter-spacing:.1em;}
ul,ol{padding-left:22px;margin:0 0 14px;}
li{margin-bottom:5px;}
table{width:100%;border-collapse:collapse;margin:6px 0 20px;font-size:13px;
background:var(--card);border:1px solid var(--rule);border-radius:7px;overflow:hidden;}
thead th{background:var(--rule-soft);text-align:left;padding:9px 11px;font-weight:700;
color:var(--ink);border-bottom:1px solid var(--rule);font-size:11px;
text-transform:uppercase;letter-spacing:.05em;}
tbody td{padding:9px 11px;border-bottom:1px solid var(--rule-soft);vertical-align:top;}
tbody tr:last-child td{border-bottom:0;}
tbody tr:nth-child(even){background:#fafbfd;}
.callout{border-left:4px solid var(--accent);background:var(--accent-soft);padding:12px 16px;
border-radius:0 7px 7px 0;margin:14px 0;font-size:14.5px;}
.callout.crit{border-left-color:var(--crit);background:var(--crit-soft);}
.callout.high{border-left-color:var(--high);background:var(--high-soft);}
.callout.med{border-left-color:var(--med);background:var(--med-soft);}
.callout.ok{border-left-color:var(--ok);background:var(--ok-soft);}
.callout.devops{border-left-color:var(--accent-2);background:var(--accent-2-soft);}
.callout-label{text-transform:uppercase;letter-spacing:.1em;font-size:10.5px;font-weight:700;
margin-bottom:3px;}
.callout.crit .callout-label{color:var(--crit);}
.callout.high .callout-label{color:var(--high);}
.callout.med .callout-label{color:var(--med);}
.callout.ok .callout-label{color:var(--ok);}
.callout.accent .callout-label{color:var(--accent);}
.callout.devops .callout-label{color:var(--accent-2);}
.sev{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;
text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;}
.sev.crit{background:var(--crit-soft);color:var(--crit);}
.sev.high{background:var(--high-soft);color:var(--high);}
.sev.med{background:var(--med-soft);color:var(--med);}
.sev.ok{background:var(--ok-soft);color:var(--ok);}
.sev.verified{background:var(--ok-soft);color:var(--ok);}
.sev.gap{background:var(--crit-soft);color:var(--crit);}
.sev.new{background:#e6f4ec;color:#1f6f43;}
.stat-row{display:flex;flex-wrap:wrap;gap:10px;margin:16px 0 6px;}
.stat{flex:1;min-width:118px;background:var(--card);border:1px solid var(--rule);
border-radius:9px;padding:12px 14px;}
.stat .num{font-size:22px;font-weight:700;line-height:1.05;color:var(--accent);}
.stat .lbl{font-size:11.5px;color:var(--muted);margin-top:4px;}
.stat.ok .num{color:var(--ok);}
.stat.crit .num{color:var(--crit);}
hr{border:0;border-top:1px solid var(--rule);margin:42px 0;}
hr.devops{border-top:2px solid var(--accent-2);}
.section-num{color:var(--accent);font-weight:700;font-size:13.5px;text-transform:uppercase;
letter-spacing:.1em;}
.section-num.devops{color:var(--accent-2);}
.pid{display:inline-block;background:var(--accent);color:#fff;font-size:10.5px;font-weight:700;
padding:2px 7px;border-radius:4px;font-family:"SF Mono",Menlo,monospace;margin-right:8px;}
.review-block{border:1px solid var(--rule);border-radius:10px;padding:16px 18px;margin:18px 0;background:var(--card);}
.review-block h4{margin:0 0 8px;color:var(--accent-2);}
.label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
color:var(--muted);margin:10px 0 3px;}
.label.crit{color:var(--crit);}
.label.ok{color:var(--ok);}
.label.high{color:var(--high);}
.part-banner{background:linear-gradient(135deg, var(--accent-soft), #fff);
border:1px solid var(--rule);border-radius:12px;padding:22px 26px;margin:32px 0 24px;}
.part-banner.devops{background:linear-gradient(135deg, var(--accent-2-soft), #fff);}
.part-banner .part-num{font-size:11px;letter-spacing:.18em;text-transform:uppercase;
color:var(--accent);font-weight:700;}
.part-banner.devops .part-num{color:var(--accent-2);}
.part-banner h2{margin:6px 0 8px;border:0;padding:0;font-size:24px;}
.part-banner p{margin:0;font-size:14.5px;color:var(--ink-soft);}
footer{margin-top:60px;padding-top:22px;border-top:1px solid var(--rule);font-size:12.5px;
color:var(--muted);text-align:center;}
@media print{
body{background:#fff;font-size:10.5pt;}
.layout{display:block;}
aside.nav{display:none;}
main.content{padding:0;max-width:none;}
h2,h3,h4{page-break-after:avoid;}
.card,.callout,table,pre,.review-block{page-break-inside:avoid;}
}
@media (max-width:900px){
.layout{grid-template-columns:1fr;}
aside.nav{position:static;height:auto;border-right:0;border-bottom:1px solid var(--rule);}
main.content{padding:28px 20px 72px;}
}
</style>
</head>
<body>
<div class="layout">
<aside class="nav">
<div class="brand">CHIL · Aurora Migration · Complete</div>
<h4>Part 1 · Why &amp; What</h4>
<ol>
<li><a href="#exec">0. Executive summary</a></li>
<li><a href="#why-aurora">1. Why Aurora</a></li>
<li><a href="#canonical">2. Canonical data model</a></li>
<li><a href="#topology">3. Target topology</a></li>
</ol>
<h4>Part 2 · Code-validated baseline</h4>
<ol>
<li><a href="#code-baseline">4. What's actually on main</a></li>
<li><a href="#discoveries">5. NEW findings from deep audit</a></li>
</ol>
<h4>Part 3 · Architecture detail</h4>
<ol>
<li><a href="#dispatcher">6. Dispatcher + Amplify connect-to-SQL</a></li>
<li><a href="#rls">7. RLS — multi-tenancy spine</a></li>
<li><a href="#frontend">8. Frontend impact (headline)</a></li>
<li><a href="#reporting">9. Reporting (Aurora reader)</a></li>
</ol>
<h4>Part 4 · Week-by-week playbook</h4>
<ol>
<li><a href="#pre-flight">10. Pre-flight checklist</a></li>
<li><a href="#phase-1">11. Phase 1 — Foundation</a></li>
<li><a href="#phase-2">12. Phase 2 — Schema + non-PHI</a></li>
<li><a href="#phase-3">13. Phase 3 — Dispatcher + tenancy</a></li>
<li><a href="#phase-4">14. Phase 4 — PHI cutover</a></li>
<li><a href="#phase-5">15. Phase 5 — Reporting + DR + closeout</a></li>
</ol>
<h4 class="devops">Part 5 · DevOps deep review</h4>
<ol>
<li><a href="#how-to-read">16. How to read DevOps items</a></li>
<li><a href="#account">17. AWS account + organization</a></li>
<li><a href="#identity-center">18. IAM Identity Center</a></li>
<li><a href="#vpc">19. VPC + network topology</a></li>
<li><a href="#kms">20. KMS keys</a></li>
<li><a href="#aurora">21. Aurora cluster config</a></li>
<li><a href="#rds-proxy">22. RDS Proxy</a></li>
<li><a href="#pg-extensions">23. Postgres extensions</a></li>
<li><a href="#pg-roles">24. Postgres roles</a></li>
<li><a href="#ddb-streams">25. DDB Streams + PITR</a></li>
<li><a href="#cognito">26. Cognito coordination</a></li>
<li><a href="#customauth">27. customAuth Lambda</a></li>
<li><a href="#lambda-vpc">28. Lambda VPC attach</a></li>
<li><a href="#pc">29. Provisioned Concurrency</a></li>
<li><a href="#cold-start">30. Cold-start mitigation</a></li>
<li><a href="#secrets">31. Secrets Manager</a></li>
<li><a href="#s3-paths">32. S3 path policies</a></li>
<li><a href="#audit-bucket">33. S3 audit-archive</a></li>
<li><a href="#cloudtrail">34. CloudTrail</a></li>
<li><a href="#waf">35. WAF v2</a></li>
<li><a href="#appsync">36. AppSync hardening</a></li>
<li><a href="#csp">37. CSP + HTTP headers</a></li>
<li><a href="#cicd">38. CI/CD security gates</a></li>
<li><a href="#drizzle-runner">39. Migration runner</a></li>
<li><a href="#yaml-bug">40. amplify.yml fixes</a></li>
<li><a href="#dashboards">41. CloudWatch dashboards</a></li>
<li><a href="#alarms">42. Alarms inventory</a></li>
<li><a href="#sentry">43. Sentry hardening</a></li>
<li><a href="#backups">44. Backup strategy</a></li>
<li><a href="#dr-drill">45. DR drill mechanics</a></li>
<li><a href="#cutover-infra">46. Cutover-temp infra</a></li>
<li><a href="#rollback">47. Rollback mechanics</a></li>
<li><a href="#decommission">48. Decommission plan</a></li>
</ol>
<h4>Part 6 · Frontend</h4>
<ol>
<li><a href="#frontend-catalog">49. Frontend change catalog</a></li>
<li><a href="#frontend-noop">50. What does NOT change</a></li>
</ol>
<h4>Part 7 · Security &amp; compliance</h4>
<ol>
<li><a href="#hipaa">51. HIPAA Safeguards schedule</a></li>
<li><a href="#soc2">52. SOC 2 criteria schedule</a></li>
<li><a href="#pre-cutover-gates">53. Pre-cutover hard requirements</a></li>
<li><a href="#evidence-pack">54. Auditor evidence pack</a></li>
</ol>
<h4>Part 8 · Closeout</h4>
<ol>
<li><a href="#cost">55. Cost</a></li>
<li><a href="#timeline">56. Timeline summary</a></li>
<li><a href="#mike-deliverables">57. Mike's deliverables</a></li>
<li><a href="#acceptance">58. Acceptance criteria</a></li>
<li><a href="#openq">59. Open questions</a></li>
</ol>
<h4>Appendix</h4>
<ol>
<li><a href="#glossary">A. Glossary</a></li>
<li><a href="#review-method">B.1 Review methodology</a></li>
<li><a href="#arc42-audit">B.2 arc42 coverage audit</a></li>
<li><a href="#c4-model">B.3 C4 model — Context / Container / Component</a></li>
<li><a href="#utility-tree">B.4 ATAM utility tree</a></li>
<li><a href="#risk-themes">B.5 ATAM risk themes</a></li>
<li><a href="#sensitivity">B.6 ATAM sensitivity &amp; tradeoff points</a></li>
<li><a href="#well-arch">B.7 AWS Well-Architected 6-pillar scoring</a></li>
<li><a href="#fitness-functions">B.8 Evolutionary fitness functions catalog</a></li>
<li><a href="#adr-inventory">B.9 ADR inventory (decisions to formalize)</a></li>
<li><a href="#maturity">B.10 Architecture maturity scorecard</a></li>
<li><a href="#gap-closure">B.11 Pre-Phase-1 gap closure</a></li>
</ol>
</aside>
<main class="content">
<header>
<div class="eyebrow">Childfree Legacy · Aurora Migration · Complete Document</div>
<h1>Aurora migration — complete plan, week-by-week playbook, DevOps deep review with code validation</h1>
<p class="lede">
Single source of truth for the Aurora migration. Combines the execution plan (what to do, when, what the frontend
feels, which HIPAA/SOC 2 controls land) with the DevOps deep architectural review (every claim code-validated, every
item with the exact CLI command Mike runs). Companion to <code>CALL-PREP-UNIFIED.html</code> (which includes
diagnosis, Track A alternative, full decision context); this one assumes Track B is chosen.
</p>
<div class="meta">
<span class="meta-chip">Date · 2026-05-26</span>
<span class="meta-chip">Timeline · 14–16 weeks</span>
<span class="meta-chip">Cost · ~$540/mo + ~$80–100k engineering</span>
<span class="meta-chip">Frontend rewrite · NO</span>
<span class="meta-chip">Code-validated · main ecd31d25b</span>
<span class="meta-chip">Audience · Jon · Mike · Oleksii</span>
</div>
</header>
<div class="part-banner">
<div class="part-num">Part 1 of 8</div>
<h2 style="border:0;color:var(--ink);">Why &amp; What — the case + the target</h2>
<p>Executive summary, six reasons to migrate, the canonical data model we're landing on, and the target architecture diagram. This is the "case for change" — short and decisive.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 0 · EXEC ================================================ -->
<!-- =============================================================== -->
<section id="exec">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 1 · 00</span>
<h2 style="border:0;margin:18px 0 0;">Executive summary</h2>
</div>
<div class="summary-card">
<h4>In one paragraph</h4>
<p style="margin:0;">
Migrate the relational core (~32 entities) from DynamoDB to Aurora Serverless v2. Keep the genuinely
key-value/event-stream data (audit events, auth attempts, verification tokens, waitlist) in DynamoDB. Use
Amplify's <code>connect-to-SQL</code> feature for the ~30 non-PHI models (managed SQL Lambda; resolvers auto-generated)
and a custom dispatcher Lambda for the ~15 PHI models (needed for per-request <code>SET LOCAL app.trust_company_id</code>
RLS — Amplify's managed SQL Lambda cannot do this). <strong>Frontend is not rewritten</strong> — the
<code>client.models.X.get/list/create/update</code> calls keep working through <code>a.combine()</code>. Non-PHI tables
migrate one-at-a-time over weeks 6–11 with no maintenance window; the PHI core flips in a single coordinated cutover
in weeks 11–13 with a 4-hour rollback window. <strong>Total calendar: 14–16 weeks. Steady-state cost increase: ~$200/mo
over current DynamoDB. One-time engineering: ~$80–100k.</strong>
</p>
</div>
<div class="stat-row">
<div class="stat"><div class="num">14–16</div><div class="lbl">weeks calendar</div></div>
<div class="stat ok"><div class="num">NO</div><div class="lbl">frontend rewrite</div></div>
<div class="stat"><div class="num">~32</div><div class="lbl">relational tables in Aurora</div></div>
<div class="stat"><div class="num">~5</div><div class="lbl">event-stream tables stay in DDB</div></div>
<div class="stat"><div class="num">~15</div><div class="lbl">PHI tables behind custom dispatcher</div></div>
<div class="stat"><div class="num">~30</div><div class="lbl">non-PHI tables via Amplify SQL</div></div>
<div class="stat"><div class="num">~$540</div><div class="lbl">monthly steady state</div></div>
<div class="stat"><div class="num">~$80–100k</div><div class="lbl">engineering one-time</div></div>
</div>
<h3>What this document does</h3>
<ul>
<li>Defines the target architecture and the canonical data model that lands in Aurora.</li>
<li>Provides a week-by-week playbook with explicit phase gates and rollback criteria.</li>
<li>Code-validates every architectural claim against the actual repository (file:line references).</li>
<li>Provides 33 deep DevOps items — each with code validation, deep thinking on failure modes, the concrete CDK/CLI deliverable, and the verification command.</li>
<li>Enumerates the exact frontend changes (mostly mechanical renames; ~45–65 files touched).</li>
<li>Maps every HIPAA Safeguard and SOC 2 Trust Service Criterion to the phase that delivers the control.</li>
<li>Lists pre-cutover hard requirements (security gates that must close before Phase 4).</li>
<li>Provides the auditor-ready evidence pack you assemble at Phase 5 close.</li>
</ul>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 1 · WHY AURORA ========================================== -->
<!-- =============================================================== -->
<section id="why-aurora">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 1 · 01</span>
<h2 style="border:0;margin:18px 0 0;">Why Aurora — six reasons (three from architecture, three from lived dev pain)</h2>
</div>
<table>
<thead><tr><th>#</th><th>Reason</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>1</td><td><strong>The data is genuinely relational.</strong></td><td>Users have documents, status histories, payments, support threads, medical incidents — every entity has FK-shaped references everywhere. Track A would permanently fight the tool to do joins; Track B uses the right tool for the shape.</td></tr>
<tr><td>2</td><td><strong>Reporting in Track B is just a read replica.</strong></td><td>Same database, same SQL, sub-second freshness. No sync pipeline. No second database to operate. Track A's reporting story is permanently a second database (Redshift via zero-ETL).</td></tr>
<tr><td>3</td><td><strong>Database-enforced referential integrity makes duplication failures structurally impossible.</strong></td><td>A foreign key cannot point at a concept with no single home; a NOT NULL cannot be satisfied by "it depends which copy"; a transaction either writes the canonical row or it doesn't. The relational migration is not just a performance/reporting move — it is the mechanism by which the data architecture becomes <em>correct</em>.</td></tr>
<tr><td>4</td><td><strong>P1 — pagination is broken at the data layer today, and the 5-table single-table Track A can't fully fix it.</strong></td><td>The 1 MB AppSync/DDB response cap truncates lists <em>before</em> filtering — empty pages with nextTokens, hard infinite scroll, unpredictable UX. Track B's <code>WHERE</code>+<code>LIMIT</code>+<code>ORDER BY</code> removes this as a class of problem.</td></tr>
<tr><td>5</td><td><strong>P3 — adding a new access pattern is too expensive in DDB.</strong></td><td>New GSI + backfill + CFN deploy interaction = the team comments code out and deploys in stages. Track B replaces this with <code>CREATE INDEX CONCURRENTLY</code> in a migration file — minutes instead of half-days.</td></tr>
<tr><td>6</td><td><strong>P4 — relational queries the devs need are awkward in DDB.</strong></td><td>"All templates of a user with form count + owner info" is one SQL statement vs. a Lambda merge of multiple queries in DDB. Every report and admin view of this shape gets cheaper.</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 2 · CANONICAL MODEL ===================================== -->
<!-- =============================================================== -->
<section id="canonical">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 1 · 02</span>
<h2 style="border:0;margin:18px 0 0;">The canonical data model — 37 entities, one home per concept</h2>
</div>
<p>
The current state is 41 <code>a.model()</code> declarations with duplications, dead code, embedded arrays and overlapping
concepts. The target is <strong>37 canonical entities</strong> — 32 relational (in Aurora) + 5 event streams (stay in
DynamoDB). Built on principles: one home per concept, tenant id on every member-scoped row, referential integrity
expressed via FKs, state changes recorded in append-only history tables, orthogonal status fields, soft delete,
append-only unified audit, no plaintext PII in keys or logs, honestly-named schema.
</p>
<h3>The 42 → 37 collapse map (selected)</h3>
<table>
<thead><tr><th>Current</th><th>Canonical</th><th>Why</th></tr></thead>
<tbody>
<tr><td><code>User</code> (60+ fields god-model)</td><td><code>user</code> (lean) + <code>user_interview</code> + <code>user_onboarding</code> + <code>user_quarterly_review</code> + <code>user_people_library</code></td><td>Decompose; identity stays lean; everything else moves to typed child entities</td></tr>
<tr><td><code>UserInterviewProgressNew</code> + <code>InterviewProgressV2</code> + V3 wrappers + <code>UserExecutionProgress</code></td><td>1 × <code>user_interview</code></td><td>Four representations → one. Two dead, two duplicates.</td></tr>
<tr><td>scalar+array <code>assignedWelonTrustId(s)</code> on User, Document, ShippingLabel, CareDocumentNew</td><td>1 × <code>welon_assignment</code> <span class="sev new">NEW</span></td><td>Eight stores → one. Real, auditable, revocable.</td></tr>
<tr><td>5 medical homes (interview blob, CareDocumentNew Medical, MedicalReport, MedicalIncident, IncapacitatedStatusLog)</td><td><code>medical_record</code> <span class="sev new">NEW canonical</span> + <code>medical_incident</code> + <code>medical_review</code> + <code>medical_note</code> + <code>incapacitation_event</code> + <code>medical_review_caseload</code> <span class="sev new">NEW</span></td><td>One home for medical wishes; caseload fixes HIPAA gap</td></tr>
<tr><td><code>Logs</code> + <code>DocumentUpdateLog</code> + <code>DocumentEventLog</code></td><td>1 × <code>audit_event</code></td><td>Three logs → one unified audit trail (stays in DDB; high volume)</td></tr>
<tr><td><code>LoginAttempt</code> + <code>mfaAttempt</code> + <code>LoginHistory</code></td><td>1 × <code>auth_attempt</code></td><td>Email replaced by <code>user_id</code> FK + email_hash (stays in DDB)</td></tr>
<tr><td>7 status fields on User</td><td><code>user.lifecycle_state</code> + <code>user.billing_state</code> + status-history tables</td><td>Orthogonal dimensions, not overlapping enums</td></tr>
<tr><td><code>SequenceNumberHelper</code> (race-prone single counter)</td><td>Postgres <code>SEQUENCE</code></td><td>Transaction-safe, gap-tolerant</td></tr>
<tr><td>Embedded <code>CareDocumentNew.data[]</code> array of 16 sections</td><td><code>care_document</code> + <code>care_document_section</code></td><td>First-class rows with own access control + versioning</td></tr>
<tr><td>Embedded <code>SupportRequest.messages[]</code></td><td><code>support_request</code> + <code>support_message</code> + <code>support_attachment</code></td><td>Same — embedded arrays become child tables</td></tr>
<tr><td><code>TestModel</code>, dead interview array, <code>UserExecutionProgress</code></td><td>DELETED</td><td>Dead code</td></tr>
</tbody>
</table>
<h3>What stays in DynamoDB (intentional)</h3>
<p>
Five entities are genuinely key-value / append-only / high-volume and stay in DDB:
<code>audit_event</code> (unified audit log; millions/month),
<code>auth_attempt</code> + <code>auth_lockout</code> (login attempts; millions/month, email-hash-centric),
<code>verification_token</code> (TTL-managed),
<code>waitlist_entry</code> + <code>waitlist_event</code> (transient queue),
<code>outbox</code> (transactional outbox; actually lives in Aurora — but the pattern is shared).
</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 3 · TOPOLOGY ============================================ -->
<!-- =============================================================== -->
<section id="topology">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 1 · 03</span>
<h2 style="border:0;margin:18px 0 0;">Target topology</h2>
</div>
<pre class="diagram"><code>Client → CloudFront → WAF v2
AppSync GraphQL (introspection OFF, depth=10)
┌──────┴───────────────────────────────────────┐
│ a.combine([ddbSchema, sqlSchema, phiSchema]) │
└──────┬─────────────────┬──────────────────┬──┘
│ │ │
│ ~30 non-PHI │ ~15 PHI models │ ~5 event streams
│ models │ resolved via │ (audit, auth_attempt,
│ → Amplify SQL │ custom dispatcher│ verification, waitlist)
│ (managed) │ Lambda │ → DynamoDB
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ Amplify SQL Lambda │ │ Custom Dispatcher │ │ DynamoDB Tables │
│ (AWS-managed) │ │ Lambda (yours) │ │ (Amplify default) │
│ │ │ │ │ │
│ chil_sqlconnect │ │ pg driver + │ │ │
│ scope = non-PHI │ │ RDS Proxy + IAM │ │ │
│ tables only │ │ │ │ │
│ │ │ BEGIN; │ │ │
│ │ │ SET LOCAL │ │ │
│ │ │ app.trust_co_id; │ │ │
│ │ │ SET LOCAL │ │ │
│ │ │ app.cognito_id; │ │ │
│ │ │ SET LOCAL │ │ │
│ │ │ app.role; │ │ │
│ │ │ SELECT col_proj │ │ │
│ │ │ FROM "user" ... │ │ │
│ │ │ COMMIT; │ │ │
│ │ │ │ │ │
│ │ │ chil_app role │ │ │
│ │ │ (no BYPASSRLS) │ │ │
└─────────┬──────────┘ └────────┬───────────┘ └─────────┬──────────┘
│ │ │
└────────────────────────┴──────────────────────────┘
┌────────────────────────────────────────────────────┐
│ Aurora Serverless v2 PostgreSQL │
│ · Writer instance (1–8 ACU) │
│ · Reader instance (0.5–4 ACU) — for reporting │
│ · pgaudit + pg_stat_statements + pgcrypto │
│ · RLS policies on PHI tables │
│ · CMK encryption at rest │
│ · TLS in transit (require_ssl=true) │
└────────────────────────────────────────────────────┘
ALSO: 74 existing Lambdas (Cognito triggers, schedulers,
webhooks, document generation, etc.) — STAY NON-VPC.
They keep calling AppSync over HTTPS exactly as today.
Only the dispatcher (and encrypt/decrypt + outbox worker)
are VPC-attached.</code></pre>
<h3>Why a single VPC-attached dispatcher Lambda (not 74)</h3>
<table>
<thead><tr><th>Reason</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>ENI exhaustion</td><td>74 functions × 50 concurrent = 3,700 ENIs. AWS soft-limit: 250 per region. Putting all 74 in the VPC blows past the cliff by &gt;10×.</td></tr>
<tr><td>Cold start delta</td><td>VPC Lambda cold starts add 1–3s vs ~200ms non-VPC. Don't pay that on every function.</td></tr>
<tr><td>NAT cost</td><td>Every VPC Lambda calling AWS APIs (Secrets, S3, SES, Stripe) needs NAT or a VPC endpoint. Multiplying across 74 functions is sprawl.</td></tr>
<tr><td>One PC pool</td><td>5–20 provisioned concurrency on one dispatcher amortizes across all model ops. ~185 access patterns (37 × 5 ops) each needing PC = $$$$.</td></tr>
<tr><td>One projection layer</td><td>The ~200 field-level auth rules live in one TS file with one test suite.</td></tr>
</tbody>
</table>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 2 of 8</div>
<h2 style="border:0;color:var(--ink);">Code-validated baseline — what's <em>actually</em> on main</h2>
<p>Every number in this document was re-verified by direct read of the repository at commit <code>ecd31d25b</code>. Prior reviews carried estimates that drifted; this is the ground truth, plus six discoveries beyond all prior reviews.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 4 · CODE BASELINE ======================================= -->
<!-- =============================================================== -->
<section id="code-baseline">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 2 · 04</span>
<h2 style="border:0;margin:18px 0 0;">What's actually on main — every metric verified</h2>
</div>
<table>
<thead><tr><th>Metric</th><th>Prior reviews said</th><th>Verified count</th><th>How verified</th></tr></thead>
<tbody>
<tr><td>Lambda functions in <code>defineBackend</code></td><td>43 / 65</td><td><strong>74</strong></td><td><code>find amplify/functions -name resource.ts | wc -l</code></td></tr>
<tr><td><code>a.model()</code> declarations</td><td>42</td><td><strong>41</strong></td><td><code>grep -rE "\.model\(" amplify/data/schemas/</code></td></tr>
<tr><td>Secondary indexes (<code>index()</code> calls)</td><td>72</td><td><strong>35</strong></td><td><code>grep -rE "^\s+index\(" amplify/data/schemas/</code></td></tr>
<tr><td><code>disableOperations('subscriptions')</code> calls</td><td>—</td><td><strong>22</strong></td><td>Phase 0 cleanup partly in place</td></tr>
<tr><td>User-model field declarations (incl. nested)</td><td>60+</td><td><strong>214 lines</strong></td><td><code>grep -cE "^\s+[a-z][a-zA-Z]+:" user-models.ts</code></td></tr>
<tr><td>S3 path policies</td><td>—</td><td><strong>6</strong></td><td>evidence, documents, care-documents, personal-id, support-requests, template-sections</td></tr>
<tr><td>Cognito triggers</td><td>5</td><td><strong>5</strong></td><td>postSignUp, preTokenGeneration, preSignIn, customMessageTrigger, customSmsSender</td></tr>
<tr><td>Function URL Lambdas</td><td>—</td><td><strong>1</strong></td><td><code>processWebhookV2</code> · <code>FunctionUrlAuthType.NONE</code></td></tr>
<tr><td>Amplify <code>secret()</code> consumers</td><td>—</td><td><strong>20+ env entries</strong></td><td>STRIPE, MAILCHIMP, UPS, GOOGLE_API, FAX_*</td></tr>
<tr><td>Lambdas with <code>tracesSampleRate: 1.0</code></td><td>—</td><td><strong>postSignUpTrigger</strong> hardcoded</td><td><code>grep -rn "tracesSampleRate" amplify/functions/</code></td></tr>
<tr><td>Lambdas with top-level <code>await getAmplifyDataClientConfig</code></td><td>—</td><td><strong>50</strong></td><td><code>grep -rln "^const { resourceConfig" amplify/functions/</code></td></tr>
<tr><td>Lambdas at 15-min timeout (max)</td><td>—</td><td><strong>5</strong></td><td>sendMedicalIncident, sendFax, sendEmailBatchesWithInterval, sendRoleBasedNotifications, check-inactive-members</td></tr>
<tr><td>WAF in <code>backend.ts</code></td><td>—</td><td><strong>None</strong></td><td><code>grep -nE "(waf|WAF|WebACL)" backend.ts</code> returns nothing</td></tr>
<tr><td>AppSync depth limit / introspection</td><td>—</td><td><strong>Not configured</strong></td><td>No <code>maxDepth</code> or <code>introspection</code> in data/resource.ts</td></tr>
<tr><td>DynamoDB Streams in backend.ts</td><td>—</td><td><strong>Not enabled</strong></td><td>No <code>StreamSpecification</code> or <code>streamSpecification</code></td></tr>
<tr><td>CI/CD security gates (SAST/SCA/secret scan)</td><td>—</td><td><strong>None</strong></td><td><code>amplify.yml</code> has none</td></tr>
<tr><td>Deletion protection on DDB</td><td>OFF</td><td><strong>OFF</strong></td><td><code>backend.ts:180</code> <code>deletionProtectionEnabled = false</code></td></tr>
<tr><td>PITR on DDB</td><td>ON</td><td><strong>ON</strong></td><td><code>backend.ts:181</code> <code>pointInTimeRecoveryEnabled = true</code></td></tr>
<tr><td>Node.js runtime</td><td>—</td><td><strong>Node 22</strong></td><td>All function <code>resource.ts</code> set <code>runtime: 22</code></td></tr>
</tbody>
</table>
<div class="callout high">
<div class="callout-label">Key corrections to prior reviews</div>
<ol style="margin:6px 0 0;">
<li>Lambda count is <strong>74, not 65 or 43</strong>. ENI math becomes 74 × 50 = 3,700, far past the 250-per-region soft limit.</li>
<li>35 secondary indexes — Phase 0 cleanup already shipped ~50% reduction. The remaining 35 will become PG indexes (cheap).</li>
<li>50 of 74 Lambdas pay top-level-await cold-start tax — that's the cold-start surface area, not 3.</li>
<li>No WAF, no AppSync hardening, no DDB Streams, no CI security gates today. <strong>All Phase 1 deliverables.</strong></li>
</ol>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 5 · DISCOVERIES ========================================= -->
<!-- =============================================================== -->
<section id="discoveries">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 2 · 05</span>
<h2 style="border:0;margin:18px 0 0;">NEW findings from this code-validation pass</h2>
</div>
<p>These come from this deep audit and were not flagged in any prior review document.</p>
<div class="review-block">
<h4>D1 · <code>amplify.yml</code> has UTF-8 curly-quote bug in backend env-var lines <span class="sev crit">CRITICAL</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify.yml:9-14</code> uses <code>“</code> and <code>”</code> (Unicode U+201C/U+201D, "smart quotes") around <code>echo</code> arguments. Example:</p>
<pre class="yaml"><code>- echo “CFLegacy_Dev_UserPoolID=$CFLegacy_Dev_UserPoolID” &gt;&gt; .env</code></pre>
<div class="label crit">DEEP THINKING</div>
<p>POSIX shell does not treat smart quotes as quote characters — it treats them as literal characters. The shell <em>does</em> still expand <code>$CFLegacy_Dev_UserPoolID</code> (variable expansion happens before quote interpretation, and smart quotes aren't quote characters anyway). So the line that's appended to <code>.env</code> looks like:</p>
<pre class="yaml"><code>“CFLegacy_Dev_UserPoolID=actual-value-here”</code></pre>
<p>That's a literal <code>“</code> at the start of the line. dotenv parsers usually skip lines that don't start with <code>A-Z_</code>, but behavior is parser-dependent. <strong>Either the env var is being silently ignored, OR it's being read with literal smart-quote characters in the value</strong>. Either is broken. The reason the system "works" today is likely that AWS Amplify Hosting injects these variables a second way (via Lambda env vars set by <code>backend.ts:298+</code>), so the .env file path is unused for runtime config.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<p>Replace smart quotes with straight quotes throughout <code>amplify.yml</code>:</p>
<pre class="sh"><code># From the repo root:
sed -i '' 's/“/"/g; s/”/"/g' amplify.yml
# Verify
grep -nE "“|”" amplify.yml # must return nothing</code></pre>
<div class="label ok">VERIFICATION</div>
<p>After the fix: trigger a build; the <code>.env</code> generated in the build step must contain valid <code>KEY=value</code> lines without smart quotes. <code>cat .env</code> in the build log should show e.g. <code>CFLegacy_Dev_UserPoolID=us-east-1_AbCdE</code>.</p>
</div>
<div class="review-block">
<h4>D2 · <code>Document.content</code> is stored as <code>a.string().required()</code> — full document body in DDB attribute <span class="sev high">HIGH</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify/data/schemas/document-models.ts:63</code>: <code>content: a.string().required(),</code> on the <code>Document</code> model. Not nullable, not S3-referenced — the document content lives inside the DDB item.</p>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>DynamoDB single-item size limit: <strong>400 KB</strong>. A generated trust, will, or POA document body can easily exceed 400 KB if it includes execution instructions, multi-state legal language, attached schedules.</li>
<li>If a write exceeds 400 KB today, DynamoDB returns <code>ValidationException: Item size has exceeded the maximum allowed size</code>. The write fails silently from a user-flow perspective; Sentry may catch it.</li>
<li>For the Aurora migration: <code>content</code> becomes a Postgres <code>text</code> column. Postgres TOAST automatically compresses and offloads large values; no 400 KB cap. <strong>The migration silently fixes this problem.</strong></li>
<li>But during the migration's dual-write phase, if DDB rejects a write that PG accepts, the two stores diverge. Detection: monitor <code>ValidationException</code> rates in CloudWatch.</li>
<li>Pre-migration mitigation: scan existing <code>Document</code> items for size; if any are &gt; 350 KB, audit them and either move to S3 immediately or accept they may already be failing on update.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="sh"><code># Scan Document table for size distribution
aws dynamodb scan \
--table-name Document-&lt;env&gt; \
--projection-expression "id, content" \
--query "Items[?size(content.S) &gt; \`300000\`]" \
--output json | jq 'length'
# If any &gt; 300 KB, list them for triage:
aws dynamodb scan ... | jq '.Items[] | select(.content.S | length &gt; 300000) | .id.S'</code></pre>
<div class="label ok">VERIFICATION</div>
<p>The scan returns the count of Documents already pushing the 400 KB ceiling. If &gt; 0 — flag for content-to-S3 refactor before cutover; otherwise the migration absorbs the risk for us.</p>
</div>
<div class="review-block">
<h4>D3 · 50 Lambdas pay top-level-await cold-start tax <span class="sev high">HIGH</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<pre class="sh"><code>$ grep -rln "^const { resourceConfig, libraryOptions } =" amplify/functions/ | wc -l
50</code></pre>
<p>Pattern in those 50 handlers:</p>
<pre class="ts"><code>const { resourceConfig, libraryOptions } =
await getAmplifyDataClientConfig(env);
Amplify.configure(resourceConfig, libraryOptions);
const client = generateClient&lt;Schema&gt;();</code></pre>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>Top-level <code>await</code> runs once per Lambda init (cold start). <code>getAmplifyDataClientConfig</code> reads from SSM/env and configures Amplify — measured at <strong>~200–500 ms per cold start</strong>.</li>
<li>50 Lambdas × 200 ms = aggregate ~10 seconds of cold-start latency across the system on a cold-start storm. Per-invocation it's invisible to one user but the aggregate user experience under burst load is real.</li>
<li>Critical paths that pay this: preTokenGeneration (Cognito 5 s budget), preSignIn, postSignUpTrigger.</li>
<li>Mitigation options: (a) bake the resourceConfig into env vars (frees the await), (b) move the await into a lazy initializer that runs on first request not on Lambda init, (c) Provisioned Concurrency for the worst paths (already in PC budget).</li>
<li>For preTokenGeneration specifically: <strong>this trigger's 5 s budget includes the AppSync call to <code>UserOnboarding.get</code></strong> which goes through this same Amplify config. Cold start + AppSync round-trip can plausibly hit 3–4 s on a cold path, leaving ~1 s of headroom. Tight.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>For the three PC Lambdas (dispatcher, preTokenGen, Stripe webhook): PC already absorbs cold-start.</li>
<li>For the other 47: bake resourceConfig into env vars (one-time refactor; ~2 day effort).</li>
<li>Add a <strong>CloudWatch Lambda Insights</strong> dashboard tracking <code>init_duration_ms</code> per function. Alarm at &gt; 2 s P95 init.</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p><code>aws logs insights start-query</code> with <code>fields @timestamp, @initDuration | sort @initDuration desc | limit 100</code>. The top of the list shows which functions pay the worst cold start; tune those.</p>
</div>
<div class="review-block">
<h4>D4 · <code>postSignUpTrigger</code> has 5 s timeout (Cognito hard limit) <span class="sev high">HIGH</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify/functions/postSignUpTrigger/resource.ts</code> sets <code>timeoutSeconds: 5</code> (Cognito's hard limit for PostConfirmation triggers).</p>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>5 s is hard, not configurable. Cognito kills the trigger at 5 s.</li>
<li>Current handler does: read SequenceNumberHelper, write User row, write UserOnboarding row, set custom Cognito attrs, call processSignUp (fire-and-forget for Mailchimp). That's already 4 round-trips to AppSync + 1 to Cognito + 1 fire-and-forget Lambda invoke.</li>
<li>Adding ANY work here is dangerous — including the planned "set <code>custom:trustCompanyId</code>" attribute write (added Cognito API call).</li>
<li>Top-level await tax (D3) eats ~200–500 ms of this. Cold start eats another 500 ms.</li>
<li>If migration adds the <code>trustCompanyId</code> setting in postSignUp and we hit cold start during a signup burst, the trigger times out — <strong>signup fails</strong>.</li>
<li>Mitigation: PC on postSignUpTrigger (~$5/mo extra). Or move the trustCompanyId setting to a side-flow that happens before/after PostConfirmation rather than inside it.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Add postSignUpTrigger to the PC list. Phase-1 PC count is 4 (dispatcher, preTokenGen, Stripe webhook, postSignUpTrigger); a 5th (customAuth) is added at Phase 3. Final post-Phase-3 PC budget per §55 cost table = <strong>~$69/mo baseline, ~$140/mo peak</strong>.</li>
<li>Time-budget the current trigger before adding work — measure P50/P95/P99 init+exec time. Alarm at &gt; 4 s.</li>
<li>Consider a CloudWatch metric: <code>PostSignupTimeoutRate</code>. Any non-zero value is a real user being denied signup.</li>
</ol>
</div>
<div class="review-block">
<h4>D5 · No DynamoDB Streams enabled today — must be turned on before cutover <span class="sev crit">CRITICAL</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<pre class="sh"><code>$ grep -nE "(Streams|streamSpecification|StreamSpecification)" amplify/backend.ts
# Returns nothing</code></pre>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>The Phase 4 cutover plan relies on DDB Streams for the CDC catch-up after the S3 export. If Streams aren't enabled, the cutover plan literally cannot work as written.</li>
<li>Enabling Streams on a populated table is non-destructive but takes effect for <em>new</em> writes only — historical changes aren't captured. So we have to: (a) enable Streams, (b) wait for confidence that they're flowing, (c) export the table state at time T, (d) replay Streams from T forward.</li>
<li>Streams retain 24 h. If cutover catch-up takes &gt; 24 h, we have data loss. The plan's "T+1:40 catch-up" assumes well under 24 h, which is realistic, but Mike should know the constraint.</li>
<li>Cost: Streams pricing is per read request unit; estimated &lt; $20/mo for our volume.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Enable Streams on all 41 DDB tables in T−1 week of Phase 4 cutover. CDK in <code>backend.ts</code>:
<pre class="ts"><code>for (const DDBtable of Object.values(amplifyDynamoDbTables)) {
DDBtable.deletionProtectionEnabled = false; // existing; flip to true for cutover wk
DDBtable.pointInTimeRecoveryEnabled = true; // existing
DDBtable.streamSpecification = { // NEW
streamViewType: 'NEW_AND_OLD_IMAGES',
};
}</code></pre></li>
<li>Verify Streams ARNs are listed: <code>aws dynamodb describe-table --table-name X-&lt;env&gt; --query "Table.LatestStreamArn"</code></li>
<li>Set up the CDC consumer Lambda (idle until cutover; just a placeholder).</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p>For each DDB table, <code>aws dynamodb describe-table</code> shows <code>StreamSpecification.StreamEnabled: true</code> and <code>LatestStreamArn</code> populated.</p>
</div>
<div class="review-block">
<h4>D6 · 5 Lambdas at 15-min (AWS max) timeout — pre-PHI-cutover infra check <span class="sev med">MEDIUM</span> <span class="sev new">NEW</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>timeoutSeconds: 900</code> on: <code>sendMedicalIncident</code>, <code>helpers/sendFax</code>, <code>helpers/sendEmailBatchesWithInterval</code>, <code>sendRoleBasedNotifications</code>, <code>scheduler-functions/check-inactive-members</code>.</p>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>15 min is AWS Lambda's hard max. If any of these need more, they fail silently.</li>
<li>These are batch/burst operations. Post-migration, the dispatcher path adds ~50–100 ms per query — on a 900-second budget that's negligible.</li>
<li>But <strong><code>sendMedicalIncident</code> includes the call to <code>getMedicalDataForReview</code></strong> which pulls the medical bundle. Post-migration, that bundle comes from <code>medical_record</code> (single canonical home) instead of two contradicting sources. Faster on average; slower worst-case (depends on JOIN plan).</li>
<li>Mike should monitor <code>Duration</code> metric on these 5 Lambdas after cutover — if any starts trending toward 900 s, investigate.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<p>CloudWatch alarm per function: <code>Duration &gt; 720000 ms</code> (12 min — leaves 3 min margin) — pages on-call.</p>
</div>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 3 of 8</div>
<h2 style="border:0;color:var(--ink);">Architecture detail — dispatcher, RLS, frontend, reporting</h2>
<p>Technical anchors. How the dispatcher works, how RLS enforces tenancy, what the frontend actually sees, how reporting on the reader endpoint plays out.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 6 · DISPATCHER ========================================== -->
<!-- =============================================================== -->
<section id="dispatcher">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 3 · 06</span>
<h2 style="border:0;margin:18px 0 0;">The dispatcher + Amplify connect-to-SQL hybrid</h2>
</div>
<h3>6.1 — Amplify connect-to-SQL for the ~30 non-PHI models</h3>
<p>
Amplify Gen 2 supports a first-party feature: <code>npx ampx generate schema-from-database</code> introspects a SQL
database and auto-generates the <code>a.model()</code> representation. Amplify auto-provisions a managed Node.js "SQL
Lambda" + AppSync resolvers. <strong>Frontend code stays identical</strong> — <code>client.models.Template.list()</code>
works whether <code>Template</code> is DDB-backed or SQL-backed.
</p>
<table>
<thead><tr><th>Capability</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>Schema introspection</td><td>One command emits <code>amplify/data/schema.sql.ts</code>; hand-curate auth rules + relationships afterwards.</td></tr>
<tr><td>Managed SQL Lambda</td><td>AWS owns the connection pool, connection retries, SSL/TLS. Deploys into VPC if the DB is in VPC.</td></tr>
<tr><td>Auto-resolvers</td><td>CRUD via <code>client.models.X</code> works through the same shape; frontend ergonomics preserved.</td></tr>
<tr><td>Mixed sources via <code>a.combine()</code></td><td>DDB + SQL + custom-dispatcher backed models coexist in a single AppSync API.</td></tr>
<tr><td>Custom SQL</td><td><code>a.handler.inlineSql('SELECT …')</code> for ad-hoc queries.</td></tr>
</tbody>
</table>
<div class="callout high">
<div class="callout-label">Why the dispatcher is needed for PHI tables (the connect-to-SQL limitation)</div>
The Amplify-managed SQL Lambda is a black box. It connects with one pooled credential and runs the resolver's SQL.
<strong>There is no hook to run <code>BEGIN; SET LOCAL app.trust_company_id = …; COMMIT</code> before each query</strong>
based on the caller's JWT. Without per-request session GUCs, RLS has nothing to filter on. <strong>So PHI tables cannot
use Amplify SQL alone</strong> — they need the custom dispatcher with <code>pg</code> driver + parameterized queries.
Non-PHI tables don't need this (no RLS enforcement required).
</div>
<h3>6.2 — The custom dispatcher Lambda (PHI path)</h3>
<pre class="ts"><code>// amplify/data-layer/data-resolver/handler.ts (the dispatcher entry point)
import { Client } from 'pg';
import { getIamAuthToken } from './rds-iam';
import { extractRole, requireTenant } from './auth';
import { getModelHandler } from './handlers';
export const handler = async (event: DataResolverEvent) =&gt; {
const tenantId = requireTenant(event.identity); // throws if missing
const cognitoId = event.identity.claims.sub;
const role = extractRole(event.identity.claims);
const client = new Client({
host: process.env.RDS_PROXY_HOST!,
user: 'chil_app',
database: 'chil',
password: await getIamAuthToken(),
ssl: { rejectUnauthorized: true },
});
await client.connect();
try {
await client.query('BEGIN');
await client.query('SET LOCAL app.trust_company_id = $1', [tenantId]);
await client.query('SET LOCAL app.cognito_id = $1', [cognitoId]);
await client.query('SET LOCAL app.role = $1', [role]);
await client.query('SET LOCAL statement_timeout = $1', ['10s']);
const modelHandler = getModelHandler(event.model);
const result = await modelHandler[event.op](client, event.args, { role });
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK').catch(() =&gt; {});
throw mapToAppSyncError(err); // hides SQL details
} finally {
await client.end();
}
};
// Per-model handler — column-projection by role:
export const userHandler = {
async get(client: Client, args: { id: string }, ctx: { role: Role }) {
const cols = projectUserColumns(ctx.role); // role → readable cols
const { rows } = await client.query(
`SELECT ${cols} FROM "user" WHERE id = $1`,
[args.id],
);
return rows[0] ?? null;
},
// list, create, update, delete...
};</code></pre>
<p>
This is ~1,500 lines hand-written. <strong>No ORM at runtime.</strong> The <code>pg</code> driver with parameterized
queries. Drizzle Kit is used only at dev-time for schema migrations (generates SQL DDL files; never imported at
runtime).
</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 7 · RLS ================================================= -->
<!-- =============================================================== -->
<section id="rls">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 3 · 07</span>
<h2 style="border:0;margin:18px 0 0;">RLS — multi-tenancy spine</h2>
</div>
<p>
Postgres Row-Level Security is the strongest tenant isolation available. The dispatcher Lambda sets per-request
session GUCs from the JWT claim; RLS policies on every PHI / member-scoped table reference those GUCs. With
<code>chil_app</code> as the connecting role (no <code>BYPASSRLS</code>), every query is automatically filtered.
<strong>An application bug that forgets a WHERE clause does not leak cross-tenant data</strong> — Postgres refuses
to return it.
</p>
<h3>Representative RLS policies</h3>
<pre class="sql"><code>-- Tenant isolation policy (applies to every PHI / member-scoped table)
ALTER TABLE "user" ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_tenant_isolation ON "user"
FOR ALL TO chil_app
USING (trust_company_id = current_setting('app.trust_company_id')::uuid)
WITH CHECK (trust_company_id = current_setting('app.trust_company_id')::uuid);
-- Owner-based read (member sees only their own)
CREATE POLICY user_owner_read ON "user"
FOR SELECT TO chil_app
USING (
trust_company_id = current_setting('app.trust_company_id')::uuid
AND (
external_id = current_setting('app.external_id')
OR current_setting('app.role') IN ('Admin', 'WelonTrust', 'MedicalReview', 'CallCenter')
)
);
-- Welon assignment ACL (intra-tenant scope)
CREATE POLICY document_welon_read ON document
FOR SELECT TO chil_app
USING (
trust_company_id = current_setting('app.trust_company_id')::uuid
AND (
user_id IN (SELECT id FROM "user" WHERE external_id = current_setting('app.external_id'))
OR EXISTS (
SELECT 1 FROM welon_assignment wa
WHERE wa.member_id = document.user_id
AND wa.welon_user_id = (SELECT id FROM "user" WHERE external_id = current_setting('app.external_id'))
AND wa.status = 'active'
)
OR current_setting('app.role') IN ('Admin', 'MedicalReview', 'CallCenter')
)
);
-- MEDICALREVIEW caseload (fixes the live HIPAA gap)
CREATE POLICY medical_report_reviewer_scoped ON medical_report
FOR SELECT TO chil_app
USING (
trust_company_id = current_setting('app.trust_company_id')::uuid
AND current_setting('app.role') = 'MedicalReview'
AND user_id IN (
SELECT member_user_id FROM medical_review_caseload
WHERE reviewer_user_id = (SELECT id FROM "user" WHERE external_id = current_setting('app.external_id'))
AND status = 'active'
)
);</code></pre>
<h3>The role hierarchy</h3>
<table>
<thead><tr><th>Role</th><th>Used by</th><th>BYPASSRLS?</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>chil_app</code></td><td>Dispatcher Lambda (via RDS Proxy IAM auth)</td><td>No</td><td>Default application role. RLS always applies.</td></tr>
<tr><td><code>chil_sqlconnect</code></td><td>Amplify SQL Lambda (managed) — non-PHI only</td><td>No</td><td>GRANT-scoped to non-PHI tables; physically cannot touch PHI.</td></tr>
<tr><td><code>chil_reporter</code></td><td>Reporting reads on Aurora reader</td><td>No</td><td>SELECT-only; RLS still applies.</td></tr>
<tr><td><code>chil_admin</code></td><td>Break-glass / manual ops</td><td>Yes</td><td>Used only via IAM Identity Center + temp credentials; every session via pgaudit.</td></tr>
<tr><td><code>chil_migrator</code></td><td>Schema migrations (DDL only)</td><td>Yes</td><td>No DML privileges. CI/CD with temp credentials.</td></tr>
</tbody>
</table>
<div class="callout">
<div class="callout-label">Field-level authorization (the 36 User auth blocks)</div>
RLS is row-level only — it cannot express the ~200 column×role rules on User. <strong>The dispatcher does column
projection</strong> per role (e.g. <code>SELECT id, email, role FROM "user" WHERE ...</code> with different column lists
per role). RLS is the row-level backstop; projection is the column-level control. The same matrix that exists in 36
<code>.authorization()</code> blocks today becomes one TypeScript file with full test coverage.
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 8 · FRONTEND IMPACT ===================================== -->
<!-- =============================================================== -->
<section id="frontend">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 3 · 08</span>
<h2 style="border:0;margin:18px 0 0;">Frontend impact — the headline answer</h2>
</div>
<div class="callout ok">
<div class="callout-label">The honest answer</div>
<strong>Frontend is NOT rewritten.</strong> <code>generateClient&lt;Schema&gt;()</code> works. <code>client.models.X.get/list/create/update</code>
works. Schema TypeScript types are still generated from <code>amplify/data/resource.ts</code>. The changes are
mechanical — mostly renames or shifts from JSON-blob reads to typed child-table reads. ~45–65 files touched. No "rewrite."
Detailed catalog in §49.
</div>
<h3>Side-by-side — before vs after</h3>
<h4>Example 1: Reading a user — IDENTICAL</h4>
<pre class="ts"><code>// BEFORE (today, DynamoDB-backed)
const { data: user } = await client.models.User.get({ id: userId });
// AFTER (Aurora-backed via dispatcher) — IDENTICAL CALL SHAPE
const { data: user } = await client.models.User.get({ id: userId });
// Same generated type. Same call. The dispatcher translates to SQL under the hood.</code></pre>
<h4>Example 2: Listing — IDENTICAL call shape, BETTER behavior</h4>
<pre class="ts"><code>// BEFORE (DDB) — 1 MB cap + post-filter bug
const { data: docs, nextToken } = await client.models.Document.list({
filter: { status: { eq: 'signed' } },
limit: 50,
});
// Issue: may return &lt;50 items with a nextToken because of 1 MB pre-filter cutoff.
// AFTER (Aurora) — IDENTICAL CALL SHAPE, predictable result
const { data: docs, nextToken } = await client.models.Document.list({
filter: { status: { eq: 'signed' } },
limit: 50,
});
// SQL: SELECT ... FROM document WHERE status = 'signed' LIMIT 50;
// Returns exactly 50 (or fewer if fewer match), with a stable cursor.</code></pre>
<h4>Example 3: A canonical-decomposition change</h4>
<pre class="ts"><code>// BEFORE — interview lives inside User as a JSON blob
const { data: user } = await client.models.User.get({ id });
const interview = user.interviewProgressV2; // stringified JSON
const parsed = typeof interview === 'string'
? JSON.parse(interview)
: interview; // double-encoding workaround
// AFTER — interview is its own typed child entity
const { data: interview } =
await client.models.UserInterview.get({ userId: id });
// Typed. No JSON parsing. No double-encoding pain.</code></pre>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 9 · REPORTING =========================================== -->
<!-- =============================================================== -->
<section id="reporting">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 3 · 09</span>
<h2 style="border:0;margin:18px 0 0;">Reporting — Aurora reader, no sync pipeline</h2>
</div>
<p>
Aurora has a built-in reader endpoint synchronously replicated from the writer (sub-second lag). Reporting queries
hit the reader; OLTP hits the writer. <strong>No zero-ETL, no Redshift, no Glue, no Kinesis</strong> — same database
family, same SQL, same engine.
</p>
<h3>Setup</h3>
<pre class="ts"><code>// amplify/backend.ts (Track B reporting)
import { aws_rds as rds } from 'aws-cdk-lib';
// Reader endpoint on the same Aurora cluster
const auroraReader = new rds.CfnDBInstance(stack, 'ChilAuroraReader', {
dbClusterIdentifier: auroraCluster.clusterIdentifier,
dbInstanceClass: 'db.serverless',
engine: 'aurora-postgresql',
});
// Add reader target group to RDS Proxy
new rds.CfnDBProxyTargetGroup(stack, 'ReaderTargetGroup', {
dbProxyName: rdsProxy.dbProxyName,
targetGroupName: 'reader',
dbInstanceIdentifiers: [auroraReader.ref],
connectionPoolConfigurationInfo: {
maxConnectionsPercent: 90,
maxIdleConnectionsPercent: 50,
connectionBorrowTimeout: 30,
},
});</code></pre>
<h3>Representative KPI query (MRR by signup cohort)</h3>
<pre class="sql"><code>-- Set tenant context (per-request)
SET LOCAL app.trust_company_id = '00000000-0000-4000-8000-000000000001';
WITH cohorts AS (
SELECT id AS user_id, DATE_TRUNC('month', created_at)::date AS cohort
FROM "user"
)
SELECT c.cohort,
COUNT(DISTINCT c.user_id) AS cohort_size,
SUM(s.amount_cents) FILTER (WHERE s.status = 'active') / 100.0 AS mrr_usd,
ROUND(
SUM(s.amount_cents) FILTER (WHERE s.status='active') /
NULLIF(COUNT(DISTINCT c.user_id) * 100.0, 0), 2) AS arpu_usd
FROM cohorts c
LEFT JOIN subscription s ON s.user_id = c.user_id
GROUP BY 1
ORDER BY 1 DESC
LIMIT 12;
-- One query. Joins. Window-style aggregation. Returns predictable bytes.</code></pre>
<p>
The Next.js Reporting page calls the dispatcher in reporting mode (using chil_reporter against the reader endpoint).
KPI cards render. Existing Apps Script prototype is decommissioned (it's an unaudited data-access surface — HIPAA risk).
</p>
<h3>Sync mechanics + cost</h3>
<table>
<thead><tr><th>Aspect</th><th>Aurora reader</th></tr></thead>
<tbody>
<tr><td>Sync mechanism</td><td>Synchronous physical replication (built-in)</td></tr>
<tr><td>Sync lag</td><td>&lt; 100 ms typical</td></tr>
<tr><td>Setup mechanism</td><td>CDK: add a reader instance to the existing cluster</td></tr>
<tr><td>Initial backfill</td><td>Snapshot + restore on creation (~minutes)</td></tr>
<tr><td>Schema evolution</td><td>Reader inherits writer schema instantly</td></tr>
<tr><td>Failure recovery</td><td>Automatic replacement; promote standby if needed</td></tr>
<tr><td>Monthly cost</td><td>~$50–150 (0.5 ACU baseline + bursts)</td></tr>
</tbody>
</table>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 4 of 8</div>
<h2 style="border:0;color:var(--ink);">Week-by-week playbook — 5 phases with phase gates</h2>
<p>Pre-flight checklist, then five phases over 14–16 weeks. Each phase lists backend deliverables, frontend impact (usually "none"), HIPAA/SOC 2 controls landed, and acceptance gate before the next phase starts.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 10 · PRE-FLIGHT ========================================= -->
<!-- =============================================================== -->
<section id="pre-flight">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 10</span>
<h2 style="border:0;margin:18px 0 0;">Pre-flight checklist (before week 1)</h2>
</div>
<div class="card">
<ol style="margin:0;">
<li><strong>Track B decided.</strong> The team has agreed Aurora is the target.</li>
<li><strong>Data architect named</strong> — owns the canonical schema, dispatcher, RLS policies. Recommended: Oleksii.</li>
<li><strong>External Cognito pool owner identified</strong> — needed for <code>custom:trustCompanyId</code> + <code>custom:onboardingCompleted</code> attribute adds across dev-test, staging, prod.</li>
<li><strong>AWS BAA confirmed</strong> on the account. Aurora Serverless v2 + RDS Proxy + KMS + Secrets Manager are HIPAA-eligible under the BAA — verify it's signed.</li>
<li><strong>Phase 0 cleanup continued</strong> in parallel (subscriptions/delete off, dead GSIs/models removed). This is rescuing the live CFN compliance issue regardless.</li>
<li><strong>Repo readiness</strong>: branch protection on <code>main</code>, required PR review, release log, CI gate.</li>
<li><strong>Migration tooling decided</strong>: <strong>Drizzle Kit</strong> (TypeScript schema + generates SQL migrations; dev-time only, no runtime dependency). Backup: Flyway (hand-written SQL files).</li>
<li><strong>BAA inventory for all data-path vendors</strong>: AWS (signed), Sentry Enterprise (verify tier), Stripe (signed), Mailchimp (signed), UPS (verify).</li>
</ol>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 11 · PHASE 1 ============================================ -->
<!-- =============================================================== -->
<section id="phase-1">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 11</span>
<h2 style="border:0;margin:18px 0 0;">Phase 1 — Foundation (Weeks 1–2)</h2>
</div>
<p>Goal: <strong>infrastructure exists and is reachable</strong>. No data movement yet. No production impact yet.</p>
<table>
<thead><tr><th>Workstream</th><th>Backend deliverable</th></tr></thead>
<tbody>
<tr><td>VPC + network</td><td>CDK in <code>amplify/backend.ts</code>: VPC (2 AZs, /16), private + public subnets, NAT × 2, IGW, route tables. VPC interface endpoints (Secrets, KMS, CloudWatch, STS), gateway endpoints (S3, DDB). Deny-by-default security groups.</td></tr>
<tr><td>KMS keys</td><td>Customer-managed CMKs per environment: <code>chil-aurora-cmk</code>, <code>chil-reporting-cmk</code>, <code>chil-secrets-cmk</code>, <code>chil-audit-archive-cmk</code>. Rotation enabled. Key policies restrict principals.</td></tr>
<tr><td>Aurora Serverless v2 cluster</td><td>PostgreSQL 16, CMK at-rest encryption. Writer 1–8 ACU, reader 0.5–4 ACU. Subnet group across private subnets. DB parameter group enables pgaudit, pgcrypto, pg_stat_statements, aws_s3 extensions. <code>require_ssl=true</code>, <code>statement_timeout=10s</code>. PITR + 7-day backups. Deletion protection ON.</td></tr>
<tr><td>RDS Proxy</td><td>IAM auth enabled (no static password). Writer + reader target groups. <code>MaxConnectionsPercent=90</code>, <code>ConnectionBorrowTimeout=30</code>.</td></tr>
<tr><td>Migration tooling</td><td>Drizzle Kit set up: <code>drizzle.config.ts</code> at repo root, migrations folder. CI step applies migrations to a throwaway PG container per PR. <code>chil_migrator</code> IAM role with DDL-only grants for prod.</td></tr>
<tr><td>Postgres roles</td><td><code>chil_app</code> (no BYPASSRLS), <code>chil_sqlconnect</code> (non-PHI scope), <code>chil_reporter</code> (reader read-only), <code>chil_admin</code> (BYPASSRLS, break-glass via temp creds), <code>chil_migrator</code>.</td></tr>
<tr><td>Cognito pool changes</td><td>Coordinate with external pool owner: add <code>custom:trustCompanyId</code> + <code>custom:onboardingCompleted</code> across 3 envs. Backfill all existing users to seed organization via admin script.</td></tr>
<tr><td>Observability</td><td>3 CloudWatch dashboards (data plane, request plane, compliance). Alarms: ACU saturation, connection saturation, RDS Proxy borrow latency, RLS denial rate. Aurora pgaudit logs → CloudWatch → S3.</td></tr>
<tr><td>Audit archive</td><td>S3 audit-archive bucket: Object Lock (Compliance mode, 7-year), CMK-encrypted, Glacier lifecycle after 180 days. Block all public access.</td></tr>
<tr><td>Sentry hardening</td><td>Deploy <code>beforeSend</code> PHI scrubber in all 3 init files. Drop <code>tracesSampleRate</code> from 1 to 0.1 in prod. <code>sendDefaultPii: false</code> on server + edge. Disable <code>enableLogs</code> on PHI-bearing routes.</td></tr>
<tr><td>preTokenGen fail-closed</td><td>Wrap trigger to <code>throw</code> on any error instead of returning event without claims. Add CloudWatch metric on token-mint exceptions.</td></tr>
</tbody>
</table>
<div class="callout ok">
<div class="callout-label">Phase 1 gate</div>
<ul style="margin:6px 0 0;">
<li>Aurora cluster reachable via Session Manager + RDS Proxy.</li>
<li>RDS Proxy connection-borrow latency &lt; 100 ms measured.</li>
<li>All 5 Postgres roles created; chil_app verified no BYPASSRLS.</li>
<li>Cognito attributes added across 3 envs; backfill complete; test user shows claim in token.</li>
<li>CI runs Drizzle migrations against a throwaway container per PR.</li>
<li>Sentry scrubber deployed; deliberate PHI test exception shows redacted.</li>
<li>preTokenGen fail-closed; deliberate missing-attribute test throws.</li>
</ul>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 12 · PHASE 2 ============================================ -->
<!-- =============================================================== -->
<section id="phase-2">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 12</span>
<h2 style="border:0;margin:18px 0 0;">Phase 2 — Canonical schema + non-PHI migration (Weeks 3–6)</h2>
</div>
<p>Goal: <strong>schema lives in Aurora; non-PHI models migrate one-by-one via Amplify connect-to-SQL.</strong> Frontend continues working through both DDB and SQL paths in parallel via <code>a.combine()</code>.</p>
<table>
<thead><tr><th>Workstream</th><th>Backend deliverable</th><th>Frontend impact</th></tr></thead>
<tbody>
<tr><td>Postgres DDL</td><td>Author full canonical DDL (~32 entities). Drizzle schemas in <code>amplify/data/schema.drizzle.ts</code>. <code>trust_company</code> seeded with one row.</td><td>None — tables sit empty in Aurora</td></tr>
<tr><td>RLS policies</td><td>Enable RLS on every PHI/member-scoped table. Tenant-isolation + owner + welon policies. RLS verification matrix (12 scenarios per table) in CI.</td><td>None</td></tr>
<tr><td>Amplify connect-to-SQL</td><td><code>npx ampx generate schema-from-database</code> emits <code>amplify/data/schema.sql.ts</code>. Hand-curate auth rules. Wire <code>a.combine([ddbSchema, sqlSchema])</code> in resource.ts.</td><td>None — same <code>client.models.X</code> calls</td></tr>
<tr><td>Migrate non-PHI models one-at-a-time</td><td>Per model (Template, TemplateVersion, TemplateSection, SectionVersion, EducationalContent, Attorney, WaitlistConfig/Entry/Log/Activation, AdminResource, PromoCode — ~30 total): copy DDB → Aurora; flip the <code>a.model()</code> definition; verify; merge per-model PR. Each PR independently reversible.</td><td><strong>Zero call-shape changes.</strong> Frontend never knows.</td></tr>
<tr><td>Status-history writes early</td><td>Wire <code>subscription_status_history</code> + <code>payment_status_history</code> writes NOW. History only accrues from when you start writing — every week of delay is unrecoverable reporting history.</td><td>None</td></tr>
<tr><td>Outbox</td><td>Create <code>outbox</code> table. Stripe webhook writes Mailchimp/SES side-effects to outbox in the same Postgres transaction. Outbox-worker Lambda drains pending rows every 30 s.</td><td>None</td></tr>
</tbody>
</table>
<div class="callout ok">
<div class="callout-label">Phase 2 gate</div>
<ul style="margin:6px 0 0;">
<li>All ~12 non-PHI models successfully migrated; frontend tests pass unchanged.</li>
<li>Status-history tables receiving live writes for ≥ 1 week.</li>
<li>RLS verification matrix passes for every migrated table.</li>
<li>Outbox worker delivers at-least-once with idempotency proof.</li>
<li>Drizzle migrations applied without manual intervention in dev-test &amp; staging.</li>
</ul>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 13 · PHASE 3 ============================================ -->
<!-- =============================================================== -->
<section id="phase-3">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 13</span>
<h2 style="border:0;margin:18px 0 0;">Phase 3 — Custom dispatcher + multi-tenancy claim flow (Weeks 7–10)</h2>
</div>
<p>Goal: <strong>PHI path is ready</strong>. Dispatcher Lambda built, tenant claim flows end-to-end. Still no PHI in Aurora yet.</p>
<table>
<thead><tr><th>Workstream</th><th>Backend deliverable</th><th>Frontend impact</th></tr></thead>
<tbody>
<tr><td>customAuth Lambda authorizer</td><td>Fill in stub. Use <code>aws-jwt-verify</code> to validate JWT, require <code>trustCompanyId</code> claim, fail-closed if missing. Surface tenant/role/externalId in <code>resolverContext</code>. <code>ttlOverride: 300</code>.</td><td>None (gate is server-side)</td></tr>
<tr><td>preTokenGen claim injection</td><td>Inject <code>trustCompanyId</code> and <code>onboardingCompleted</code> as access-token claims. Remove the AppSync round-trip during token mint (denormalize via Cognito attribute).</td><td>Frontend reads <code>trustCompanyId</code> from access token via a thin helper</td></tr>
<tr><td>Dispatcher Lambda skeleton</td><td>Build <code>amplify/data-layer/data-resolver/handler.ts</code>: VPC-attached, <code>pg</code> driver via RDS Proxy with IAM auth, BEGIN/SET LOCAL/COMMIT wrapper. Per-model handler files. Field-projection map.</td><td>None — dispatcher routes only to a.query()/a.mutation() entries that don't exist yet</td></tr>
<tr><td>Field-level encryption</td><td>Design + build encrypt-field/decrypt-field Lambdas. Per-env CMK. AWS Encryption SDK envelope encryption. Encryption context binds ciphertext to userId. Caller authorization in decrypt handler. <code>kmsv1:</code> ciphertext marker.</td><td>Frontend gains thin wrapper <code>encryptField/decryptField</code> for Digital Assets credentials, SSN, bank, insurance, medical directives. No UX change.</td></tr>
<tr><td>Provisioned Concurrency</td><td>5 Lambdas at Phase 3 close: dispatcher (5 baseline, 20 max), preTokenGen (3), Stripe webhook (2), customAuth (3), postSignUpTrigger (1). ~$69/mo baseline; ~$140/mo peak — see §55 cost table.</td><td>Visible UX win: P95 sign-in latency drops 1–3 s</td></tr>
<tr><td>MEDICALREVIEW caseload</td><td>Build <code>medical_review_caseload</code> table; backfill from current ad-hoc assignments (manual triage). RLS policy restricts <code>medical_report</code> reads to caseload-scoped members.</td><td>Medical review list shortens to reviewer's caseload. <strong>Verify with the medical team before deploy.</strong></td></tr>
<tr><td>MemberNote tightening</td><td>Auth rule joins <code>welon_assignment</code> — welon reads/writes notes only for assigned members.</td><td>Welon admin sees only assigned members' notes. Verify with welon team.</td></tr>
<tr><td>Plaintext-PII column refactor (9 tables)</td><td>Migrate Logs, LoginAttempt, LoginHistory, mfaAttempt, DocumentEventLog, AgreementAcceptance, EvidenceSubmission, SupportRequest, UserInvite: replace plaintext email with <code>user_id</code> FK + <code>email_hash</code> + <code>email_last4</code>.</td><td>Support / admin screens that lookup by email continue via email_last4 + name. Lookup URL params change from email to user_id.</td></tr>
<tr><td>Step-up MFA on sensitive ops</td><td>Sign document, delete user, role change, medical export require fresh MFA (token <code>auth_time</code> within 15 min).</td><td>Frontend MFA re-challenge modal (~50 lines). Handle freshness rejection.</td></tr>
</tbody>
</table>
<div class="callout ok">
<div class="callout-label">Phase 3 gate</div>
<ul style="margin:6px 0 0;">
<li>Tenant-isolation verification matrix passing on dev-test for all 15 PHI tables.</li>
<li>Sign-in P95 latency &lt; 2 s end-to-end with PC enabled.</li>
<li>Field-level encryption round-trip verified for Digital Assets credentials.</li>
<li>MEDICALREVIEW caseload + MemberNote auth changes verified with respective teams.</li>
<li>Plaintext-email migration complete on the 9 tables; email_last4 support-triage UX validated.</li>
</ul>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 14 · PHASE 4 ============================================ -->
<!-- =============================================================== -->
<section id="phase-4">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 14</span>
<h2 style="border:0;margin:18px 0 0;">Phase 4 — PHI cutover (Weeks 11–13)</h2>
</div>
<p>Goal: <strong>PHI tables flip from DDB to Aurora in a coordinated window.</strong> Only "real cutover" — all other models migrated independently in Phase 2.</p>
<table>
<thead><tr><th>Step</th><th>Detail</th></tr></thead>
<tbody>
<tr><td><strong>T−2 weeks</strong></td><td>Dress rehearsal in dev-test with prod-shaped synthetic data. Time every step; rollback drilled (must complete &lt; 1 h on dev-test → 4 h margin in prod).</td></tr>
<tr><td><strong>T−1 week</strong></td><td>Deploy dispatcher Lambda to prod (dark — not yet routed). Deploy AppSync changes that route PHI resolvers to dispatcher (dark). Enable DynamoDB Streams on all PHI tables.</td></tr>
<tr><td><strong>T−24h</strong></td><td>Final security checks. <code>chil_app</code> verified no BYPASSRLS. CMK rotations on. pgaudit flowing. Object Lock verified on archive bucket. Cognito attribute confirmed on every user. Stripe webhook temp-503 wiring prepped.</td></tr>
<tr><td><strong>T+0 Fri 21:00 UTC</strong></td><td>Stripe + UPS webhooks routed to temporary 503 endpoint (Stripe retries; idempotent). Next.js middleware returns 503 except /health.</td></tr>
<tr><td><strong>T+0:02</strong></td><td>S3 export of PHI DDB tables (PITR-based, no live throttle).</td></tr>
<tr><td><strong>T+0:25</strong></td><td>Verify all exports succeeded. Capture timestamps.</td></tr>
<tr><td><strong>T+0:30</strong></td><td>ETL into Aurora (Lambda fanout reads S3 export, transforms, bulk-loads via <code>COPY</code>). Largest tables: User, Document, LoginHistory.</td></tr>
<tr><td><strong>T+1:30</strong></td><td>ETL complete. Row counts + per-table SHA-256 hashes match within tolerance.</td></tr>
<tr><td><strong>T+1:40</strong></td><td>CDC catch-up: read DDB Streams from cutover-start; replay into Aurora.</td></tr>
<tr><td><strong>T+2:00</strong></td><td>Verification battery: RLS tests, random-sample integrity (100 users + docs), fixed 25-query battery. Both stores agree.</td></tr>
<tr><td><strong>T+2:30</strong></td><td>Flip CFN parameter: AppSync PHI resolvers route to dispatcher. <strong>No frontend deploy needed</strong> — same endpoint, same schema.</td></tr>
<tr><td><strong>T+2:45</strong></td><td>Read-only smoke: test member signs in, decodes JWT, loads dashboard, opens medical incident. Verify <code>trustCompanyId</code> claim flowed end-to-end.</td></tr>
<tr><td><strong>T+3:00</strong></td><td>Lift maintenance mode. Webhooks re-enabled. Monitor: error rate, P95 latency, RLS denial rate (must be 0).</td></tr>
<tr><td><strong>T+3:00 → T+4:00</strong></td><td>Rollback window. If criteria below trip — flip CFN parameter back to DDB resolvers. DDB tables remained hot the whole time. Invisible to clients.</td></tr>
<tr><td><strong>T+4:00</strong></td><td>Cutover declared successful. DDB PHI tables remain in place 30 days (read-only), then deleted.</td></tr>
</tbody>
</table>
<div class="callout crit">
<div class="callout-label">Rollback decision criteria</div>
<table style="margin:6px 0 0;">
<thead><tr><th>Signal</th><th>Threshold</th><th>Action</th></tr></thead>
<tbody>
<tr><td>RLS denial rate (SQLSTATE 42501)</td><td>&gt; 0.1%</td><td>Rollback — claim flow broken</td></tr>
<tr><td>AppSync 5xx rate</td><td>&gt; 2% sustained 5 min</td><td>Rollback</td></tr>
<tr><td>P95 dashboard load latency</td><td>&gt; 3 s (was &lt; 800 ms)</td><td>Investigate first; rollback if not resolved in 30 min</td></tr>
<tr><td>Dispatcher error rate</td><td>&gt; 1%</td><td>Rollback</td></tr>
<tr><td>Sentry "tenant claim missing" events</td><td>Any</td><td>Rollback immediately — auth broken</td></tr>
<tr><td>Aurora connection saturation</td><td>&gt; 80%</td><td>Scale ACUs first; rollback if scaling fails</td></tr>
</tbody>
</table>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 15 · PHASE 5 ============================================ -->
<!-- =============================================================== -->
<section id="phase-5">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 4 · 15</span>
<h2 style="border:0;margin:18px 0 0;">Phase 5 — Reporting + DR + closeout (Weeks 14–16)</h2>
</div>
<p>Goal: <strong>reporting on the reader, DR Drill recorded, audit evidence pack complete.</strong></p>
<table>
<thead><tr><th>Workstream</th><th>Backend deliverable</th><th>Frontend impact</th></tr></thead>
<tbody>
<tr><td>Reader endpoint live</td><td>Aurora reader reachable via RDS Proxy reader target group. <code>chil_reporter</code> reads via reader; OLTP via writer.</td><td>None</td></tr>
<tr><td>Reporting page</td><td>Next.js Reporting page calls dispatcher in reporting mode. KPI queries against reader: MRR by cohort, document funnel, welon caseload, medical incident SLA, Stripe failure analysis. Decommission Apps Script prototype.</td><td>New Reporting page in <code>app/(protected)/admin/reports/</code>. Uses existing <code>client.queries.*</code> pattern.</td></tr>
<tr><td>DR Drill</td><td>Parallel Aurora cluster from PITR snapshot in sibling VPC. Shadow AppSync at <code>dr.chil.welon</code>. Readiness probe battery (row counts ±1%, RLS tests, 25-query fidelity, cold-start). RTO/RPO recorded. Report filed in Object-Lock S3. Tear down.</td><td>None — prod is untouched</td></tr>
<tr><td>Audit evidence pack</td><td>Compile: tested DR report (dated), tenant-isolation matrix sign-off, audit-log retention proof, encryption inventory, access-control matrix, BAA inventory, pen-test report, SAST/SCA/secret-scan reports, change-control PR evidence.</td><td>None</td></tr>
<tr><td>Decommission old DDB</td><td>After 30-day stability: zero traffic to old PHI DDB tables; backup snapshot to S3 Object-Lock; delete.</td><td>None</td></tr>
</tbody>
</table>
<div class="callout ok">
<div class="callout-label">Phase 5 gate (closeout)</div>
<ul style="margin:6px 0 0;">
<li>DR Drill report shows RTO &lt; 4 h, RPO &lt; 5 min, filed in S3.</li>
<li>Reporting page replaces Apps Script prototype.</li>
<li>Full evidence pack assembled; auditor walkthrough scheduled.</li>
<li>Old DDB PHI tables deleted after 30-day stability.</li>
<li>Two weeks of stable production with zero RLS denial alarms.</li>
</ul>
</div>
</section>
<hr class="devops">
<div class="part-banner devops">
<div class="part-num">Part 5 of 8 · DevOps deep review</div>
<h2 style="border:0;color:var(--ink);">Every claim code-validated, every item with the exact command</h2>
<p>33 deep-review items covering AWS account, IAM, VPC, KMS, Aurora, RDS Proxy, Postgres, DDB, Cognito, customAuth, Lambda VPC, PC, cold-start, Secrets, S3, audit-archive, CloudTrail, WAF, AppSync, CSP, CI/CD, dashboards, alarms, Sentry, backups, DR, cutover infra, rollback, decommission. Each follows the same template: CLAIM → CODE VALIDATION → DEEP THINKING → DEVOPS DELIVERABLE → VERIFICATION.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 16 · HOW TO READ ======================================== -->
<!-- =============================================================== -->
<section id="how-to-read">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 16</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">How to read DevOps items</h2>
</div>
<div class="review-block">
<h4>Item template</h4>
<div class="label">CLAIM</div>
<p style="margin:0;">The assertion from the migration plan or industry best practice.</p>
<div class="label">CODE VALIDATION</div>
<p style="margin:0;">Direct quote or reference from <code>amplify/...</code> on current main. <span class="sev verified">VERIFIED</span> = matches plan; <span class="sev gap">GAP</span> = plan assumed it; <span class="sev new">NEW</span> = discovered in code, not in any prior review.</p>
<div class="label crit">DEEP THINKING</div>
<p style="margin:0;">Failure modes, edge cases, hidden assumptions, security implications, second-order effects.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<p style="margin:0;">Concrete action(s) Mike has to take. Where possible, the exact CLI command or CDK snippet.</p>
<div class="label ok">VERIFICATION</div>
<p style="margin:0;">How to prove the action is complete.</p>
</div>
<div class="callout devops">
<div class="callout-label">Severity legend</div>
<span class="sev crit">CRITICAL</span> blocks Phase 1 if not done · &nbsp;
<span class="sev high">HIGH</span> blocks PHI cutover (Phase 4) · &nbsp;
<span class="sev med">MED</span> needed before Phase 5 closeout · &nbsp;
<span class="sev verified">VERIFIED</span> · <span class="sev gap">GAP</span> · <span class="sev new">NEW</span>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 17 · ACCOUNT ============================================ -->
<!-- =============================================================== -->
<section id="account">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 17</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">AWS account + organization</h2>
</div>
<div class="review-block">
<h4>17.1 · AWS BAA confirmed for the account <span class="sev crit">CRITICAL · pre-Phase-1</span></h4>
<div class="label">DEEP THINKING</div>
<p>AWS HIPAA-eligible services require a signed BAA between the AWS account owner and AWS. The BAA covers all listed services (Aurora, DynamoDB, Lambda, S3, KMS, CloudWatch, AppSync, Cognito, Secrets Manager, RDS Proxy, etc.). Without it, every "we encrypt at rest with KMS" claim is non-compliant. <strong>An auditor will ask for the signed BAA before any technical control evidence.</strong></p>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Confirm with AWS account owner (or via Artifact in the AWS Console): the BAA is signed and current.</li>
<li>Download the signed BAA PDF; store in the audit-archive S3 bucket alongside other compliance evidence.</li>
<li>List of services to verify on the BAA: Aurora Serverless v2 PostgreSQL, RDS Proxy, DynamoDB, Lambda, S3, KMS, Secrets Manager, CloudWatch, AppSync, Cognito, CloudTrail, EventBridge, SES, IAM.</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p>AWS Artifact UI shows BAA in "active" status. PDF in audit-archive S3 bucket.</p>
</div>
<div class="review-block">
<h4>17.2 · AWS Organizations + SCPs (region restrict) <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>HIPAA "minimum necessary" applies to infrastructure. Restrict regions to <code>us-east-1</code> and <code>us-west-2</code> (DR region).</li>
<li>Without SCPs, a misconfigured Lambda could spin up resources in a non-BAA-covered region.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="sh"><code>{
"Version": "2012-10-17",
"Statement": [{
"Sid": "RestrictToUSRegions", "Effect": "Deny",
"NotAction": ["cloudfront:*","iam:*","organizations:*","route53:*","support:*","sts:*","waf:*","wafv2:*"],
"Resource": "*",
"Condition": { "StringNotEqualsIfExists": { "aws:RequestedRegion": ["us-east-1","us-west-2"] } }
}]
}</code></pre>
<div class="label ok">VERIFICATION</div>
<p>Attempt to provision a resource in <code>eu-west-1</code> — must be denied.</p>
</div>
<div class="review-block">
<h4>17.3 · Separate accounts per environment <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<p>HIPAA Administrative Safeguards require environmental separation. Confirm whether dev-test/staging/prod are separate AWS accounts. If one shared account: scope a follow-up project to split.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 18 · IDENTITY CENTER ==================================== -->
<!-- =============================================================== -->
<section id="identity-center">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 18</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">IAM Identity Center (human access)</h2>
</div>
<div class="review-block">
<h4>18.1 · No long-lived IAM users for humans <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>HIPAA §164.312(a)(2)(i) requires Unique User ID. SOC 2 CC6 requires least-privilege. Long-lived IAM keys are anti-pattern.</li>
<li>IAM Identity Center issues temporary credentials via OIDC. Each human gets SSO; permissions via permission sets.</li>
<li>Permission sets: <strong>Developer</strong> (read-only on prod data, read-write on dev-test), <strong>DevOps</strong> (Mike — full admin), <strong>SecurityOfficer</strong> (audit-archive read, CloudTrail read), <strong>BreakGlassAdmin</strong> (production write; every action audited).</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Provision IAM Identity Center in the management account.</li>
<li>Create 4 permission sets.</li>
<li>Assign team members per-env.</li>
<li>Deactivate existing long-lived IAM user access keys (audit via IAM Access Analyzer).</li>
<li>Document break-glass procedure: who can use BreakGlassAdmin, what's logged, who notices.</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p><code>aws iam list-users | jq '.Users[] | select(.UserName != "amplify-cli-user")'</code> returns no human users.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 19 · VPC ================================================ -->
<!-- =============================================================== -->
<section id="vpc">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 19</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">VPC + network topology</h2>
</div>
<div class="review-block">
<h4>19.1 · VPC sizing and subnet layout <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>2 AZs minimum for Aurora multi-AZ + RDS Proxy + Lambda VPC attach.</li>
<li>CIDR: /16 (65,536 IPs). Subnets: 2× private /20 (~4,096 IPs each), 2× public /24 for NAT.</li>
<li>Dispatcher reserved-concurrency 100 — each ENI claims 1 private-subnet IP. 4,096 IPs is comfortable.</li>
<li>Avoid the default VPC. Dedicated VPC per environment.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const vpc = new ec2.Vpc(stack, `ChilVpc-${env}`, {
ipAddresses: ec2.IpAddresses.cidr('10.40.0.0/16'), // dev-test: 10.40 / staging: 10.50 / prod: 10.60
maxAzs: 2,
natGateways: 2,
subnetConfiguration: [
{ cidrMask: 20, name: 'private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
{ cidrMask: 24, name: 'public', subnetType: ec2.SubnetType.PUBLIC },
],
enableDnsHostnames: true,
enableDnsSupport: true,
});</code></pre>
</div>
<div class="review-block">
<h4>19.2 · VPC Endpoints (avoid NAT costs) <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>NAT Gateway: ~$0.045/hr + $0.045/GB. Interface VPC endpoints: ~$0.01/hr per endpoint per AZ + $0.01/GB.</li>
<li>For frequent AWS API calls (KMS, Secrets), interface endpoints &lt; NAT cost.</li>
<li>Gateway endpoints (S3, DynamoDB) are FREE.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>vpc.addInterfaceEndpoint('SecretsEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER });
vpc.addInterfaceEndpoint('KmsEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.KMS });
vpc.addInterfaceEndpoint('LogsEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS });
vpc.addInterfaceEndpoint('StsEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.STS });
vpc.addGatewayEndpoint('S3Gateway', { service: ec2.GatewayVpcEndpointAwsService.S3 });
vpc.addGatewayEndpoint('DdbGateway', { service: ec2.GatewayVpcEndpointAwsService.DYNAMODB });</code></pre>
</div>
<div class="review-block">
<h4>19.3 · Security groups — deny-by-default <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<p>3 SGs: <code>chil-dispatcher-sg</code>, <code>chil-rdsproxy-sg</code>, <code>chil-aurora-sg</code>. Reference by SG-to-SG rules (not CIDR). Dispatcher → RDS Proxy → Aurora. No direct dispatcher-to-Aurora.</p>
<div class="label ok">VERIFICATION</div>
<p>From a test EC2 in dispatcher SG: <code>nc -zv &lt;aurora-direct-endpoint&gt; 5432</code> must fail. <code>nc -zv &lt;rds-proxy-endpoint&gt; 5432</code> must succeed.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 20 · KMS ================================================ -->
<!-- =============================================================== -->
<section id="kms">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 20</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">KMS keys — one per concern</h2>
</div>
<div class="review-block">
<h4>20.1 · 4 keys per environment (12 total) <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>HIPAA recommends customer-managed CMKs over AWS-managed default. One key per concern:
<ol>
<li><strong>Aurora CMK</strong> — encrypts cluster + snapshots + reader.</li>
<li><strong>S3 PHI buckets CMK</strong> — existing storage + audit-archive bucket.</li>
<li><strong>Secrets Manager CMK</strong> — encrypts Amplify <code>secret()</code> entries.</li>
<li><strong>Field-encryption CMK</strong> — Digital Assets credentials, SSN, bank, insurance, medical directives. Only encrypt-field/decrypt-field Lambdas have <code>kms:Decrypt</code>.</li>
</ol>
</li>
<li>Existing <code>KMS_KEY_ID</code> for <code>customSmsSender</code> stays separate (Cognito service is the only principal).</li>
<li>Multi-region: Aurora CMK + field-encryption CMK should be multi-region for cross-region DR snapshot copy.</li>
<li>Annual rotation enabled on all 4.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const auroraKey = new kms.Key(stack, `AuroraKey-${env}`, { alias: `alias/chil-aurora-${env}`, enableKeyRotation: true });
const s3PhiKey = new kms.Key(stack, `S3PhiKey-${env}`, { alias: `alias/chil-s3-phi-${env}`, enableKeyRotation: true });
const secretsKey = new kms.Key(stack, `SecretsKey-${env}`, { alias: `alias/chil-secrets-${env}`, enableKeyRotation: true });
const fieldEncKey = new kms.Key(stack, `FieldEncKey-${env}`, { alias: `alias/chil-field-encryption-${env}`, enableKeyRotation: true });</code></pre>
</div>
<div class="review-block">
<h4>20.2 · Field-encryption CMK key policy is THE security gate <span class="sev crit">CRITICAL</span></h4>
<div class="label">DEEP THINKING</div>
<p>This is the difference between "PHI is encrypted" and "PHI is recoverable by anyone with DDB-table access." If the application's main IAM role can decrypt with this key, defense-in-depth collapses.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>// Allow encrypt-field + decrypt-field Lambdas only
fieldEncKey.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(backend.encryptField.resources.lambda.role!.roleArn),
new iam.ArnPrincipal(backend.decryptField.resources.lambda.role!.roleArn),
],
actions: ['kms:Encrypt','kms:Decrypt','kms:GenerateDataKey'],
resources: ['*'],
}));
// Explicitly DENY the main application role
fieldEncKey.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.DENY,
principals: [new iam.ArnPrincipal(backend.dataLayer.resources.lambda.role!.roleArn)],
actions: ['kms:*'], resources: ['*'],
}));</code></pre>
<div class="label ok">VERIFICATION</div>
<p>From dispatcher Lambda role: attempt <code>aws kms encrypt --key-id alias/chil-field-encryption-prod --plaintext test</code> — must be denied. From encrypt-field role — must succeed.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 21 · AURORA ============================================= -->
<!-- =============================================================== -->
<section id="aurora">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 21</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Aurora cluster — every parameter matters</h2>
</div>
<div class="review-block">
<h4>21.1 · Engine version + serverless v2 capacity <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>PostgreSQL 16 (latest LTS available on Aurora SLv2). Avoid 14.</li>
<li>Capacity: writer 1–8 ACU, reader 0.5–4 ACU. 1 ACU ≈ 2 GB RAM + matched CPU.</li>
<li>SLv2 doesn't fully pause; min ACU is the floor (0.5 ACU ≈ $22/mo idle).</li>
<li>Snapshot retention: 30 days for HIPAA evidence. Long-tail to S3.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const cluster = new rds.DatabaseCluster(stack, `Aurora-${env}`, {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_4,
}),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [auroraSecurityGroup],
serverlessV2MinCapacity: env === 'prod' ? 1 : 0.5,
serverlessV2MaxCapacity: env === 'prod' ? 8 : 4,
writer: rds.ClusterInstance.serverlessV2('writer'),
readers: [rds.ClusterInstance.serverlessV2('reader1', { scaleWithWriter: false })],
storageEncryptionKey: auroraKey,
deletionProtection: env === 'prod',
backupRetention: cdk.Duration.days(env === 'prod' ? 30 : 7),
parameters: {
'rds.force_ssl': '1',
'statement_timeout': '10000',
'shared_preload_libraries': 'pg_stat_statements,pgaudit',
'pgaudit.log': 'write, ddl',
'pgaudit.log_relation': '1',
},
});</code></pre>
</div>
<div class="review-block">
<h4>21.2 · Storage tier — Standard vs I/O-Optimized <span class="sev med">MEDIUM</span></h4>
<div class="label">DEEP THINKING</div>
<p>Standard: $0.10/GB-mo + $0.20/M I/O. I/O-Optimized: 30% ACU premium, no per-I/O charge. Crossover: when I/O bill exceeds 30% of ACU bill. <strong>Start on Standard; flip when needed.</strong></p>
</div>
<div class="review-block">
<h4>21.3 · Performance Insights + pg_stat_statements <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<p>Without PI, "the dashboard is slow" is guesswork. pg_stat_statements is foundational for tuning.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>new rds.ClusterInstance.serverlessV2('writer', {
enablePerformanceInsights: true,
performanceInsightsRetention: rds.PerformanceInsightRetention.DAYS_7,
performanceInsightEncryptionKey: auroraKey,
});</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 22 · RDS PROXY ========================================== -->
<!-- =============================================================== -->
<section id="rds-proxy">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 22</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">RDS Proxy — IAM-auth, two target groups</h2>
</div>
<div class="review-block">
<h4>22.1 · IAM authentication, no static password <span class="sev crit">CRITICAL</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>RDS Proxy supports IAM-token auth: Lambda execution role gets <code>rds-db:connect</code>; token generated at connection time. <strong>No password stored anywhere.</strong></li>
<li>HIPAA §164.312(d) Person/Entity Authentication. IAM-token auth &gt; password auth.</li>
<li>Token TTL: 15 min. Lambda connections never live that long; renewal automatic.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const proxy = new rds.DatabaseProxy(stack, `RdsProxy-${env}`, {
proxyTarget: rds.ProxyTarget.fromCluster(cluster),
secrets: [cluster.secret!],
vpc,
iamAuth: true,
securityGroups: [rdsProxySecurityGroup],
requireTLS: true,
idleClientTimeout: cdk.Duration.seconds(1800),
borrowTimeout: cdk.Duration.seconds(30),
maxConnectionsPercent: 90,
});</code></pre>
<div class="label ok">VERIFICATION</div>
<p><code>aws rds generate-db-auth-token --hostname &lt;proxy-host&gt; --port 5432 --username chil_app</code> returns a token. <code>psql "host=&lt;proxy&gt; port=5432 user=chil_app password=&lt;token&gt; dbname=chil sslmode=require"</code> succeeds.</p>
</div>
<div class="review-block">
<h4>22.2 · Writer + reader target groups <span class="sev high">HIGH</span></h4>
<div class="label">DEEP THINKING</div>
<p>Separate target groups isolate reporting load from OLTP. Reporting connects via reader target group using chil_reporter.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 23 · PG EXTENSIONS ====================================== -->
<!-- =============================================================== -->
<section id="pg-extensions">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 23</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Postgres extensions</h2>
</div>
<div class="review-block">
<h4>23.1 · Required extensions</h4>
<table>
<thead><tr><th>Extension</th><th>Purpose</th><th>HIPAA tie-in</th></tr></thead>
<tbody>
<tr><td><code>pgaudit</code></td><td>Logs SELECT/INSERT/UPDATE/DELETE/DDL on tagged tables → CloudWatch via Aurora log export</td><td>§164.312(b) Audit Controls</td></tr>
<tr><td><code>pg_stat_statements</code></td><td>Aggregates query stats; foundational for perf tuning</td><td>SOC 2 CC4 Monitoring</td></tr>
<tr><td><code>pgcrypto</code></td><td><code>gen_random_uuid()</code>, <code>digest()</code> — UUID PKs and email hashing</td><td>§164.312(a)(2)(iv) Encryption tooling</td></tr>
<tr><td><code>aws_s3</code></td><td><code>aws_s3.query_export_to_s3()</code> — audit-event export + bulk reporting UNLOAD</td><td>Audit retention pipeline</td></tr>
</tbody>
</table>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="sql"><code>CREATE EXTENSION IF NOT EXISTS pgaudit;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS aws_s3 CASCADE;
ALTER SYSTEM SET pgaudit.log = 'write, ddl';
ALTER SYSTEM SET pgaudit.log_relation = on;
ALTER SYSTEM SET pgaudit.log_statement = 'on';
-- Per-table audit (more granular) for PHI tables:
ALTER TABLE "user" SET (pgaudit.log = 'all');
ALTER TABLE medical_record SET (pgaudit.log = 'all');
ALTER TABLE medical_incident SET (pgaudit.log = 'all');
ALTER TABLE member_note SET (pgaudit.log = 'all');
ALTER TABLE document SET (pgaudit.log = 'write');</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 24 · PG ROLES =========================================== -->
<!-- =============================================================== -->
<section id="pg-roles">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 24</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Postgres roles + grants</h2>
</div>
<div class="review-block">
<h4>24.1 · Five roles, one purpose each</h4>
<table>
<thead><tr><th>Role</th><th>Used by</th><th>BYPASSRLS</th><th>Grants</th></tr></thead>
<tbody>
<tr><td><code>chil_app</code></td><td>Dispatcher Lambda (PHI path)</td><td><strong>No</strong></td><td>SELECT/INSERT/UPDATE/DELETE on all PHI tables</td></tr>
<tr><td><code>chil_sqlconnect</code></td><td>Amplify-managed SQL Lambda</td><td>No</td><td>NON-PHI tables only</td></tr>
<tr><td><code>chil_reporter</code></td><td>Dispatcher (reporting mode, reader)</td><td>No</td><td>SELECT-only on views</td></tr>
<tr><td><code>chil_admin</code></td><td>Break-glass via Identity Center temp creds</td><td><strong>Yes</strong></td><td>All</td></tr>
<tr><td><code>chil_migrator</code></td><td>CI/CD only (Drizzle migrations)</td><td>Yes (for DDL)</td><td>DDL only; no DML</td></tr>
</tbody>
</table>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>chil_sqlconnect physically cannot see PHI — this is the security boundary that makes the hybrid (Amplify SQL + dispatcher) safe.</li>
<li>chil_admin's BYPASSRLS is dangerous; every session must be audited via pgaudit; CloudWatch Insights alarm on usage.</li>
<li>chil_migrator without DML — data fixes via chil_admin with explicit audit.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="sql"><code>CREATE ROLE chil_app LOGIN;
CREATE ROLE chil_sqlconnect LOGIN;
CREATE ROLE chil_reporter LOGIN;
CREATE ROLE chil_admin LOGIN BYPASSRLS;
CREATE ROLE chil_migrator LOGIN;
GRANT USAGE ON SCHEMA public TO chil_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO chil_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO chil_app;
GRANT USAGE ON SCHEMA public TO chil_sqlconnect;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO chil_sqlconnect;
REVOKE ALL ON "user" FROM chil_sqlconnect;
REVOKE ALL ON medical_record FROM chil_sqlconnect;
REVOKE ALL ON medical_incident FROM chil_sqlconnect;
REVOKE ALL ON medical_report FROM chil_sqlconnect;
REVOKE ALL ON member_note FROM chil_sqlconnect;
REVOKE ALL ON document FROM chil_sqlconnect;
REVOKE ALL ON support_request FROM chil_sqlconnect;
REVOKE ALL ON support_message FROM chil_sqlconnect;
REVOKE ALL ON support_attachment FROM chil_sqlconnect;
REVOKE ALL ON evidence_submission FROM chil_sqlconnect;
REVOKE ALL ON care_document FROM chil_sqlconnect;
REVOKE ALL ON care_document_section FROM chil_sqlconnect;
REVOKE ALL ON medical_note FROM chil_sqlconnect;
REVOKE ALL ON emergency_contact FROM chil_sqlconnect;
REVOKE ALL ON incapacitation_event FROM chil_sqlconnect;</code></pre>
<div class="label ok">VERIFICATION</div>
<pre class="sql"><code>SET ROLE chil_sqlconnect;
SELECT count(*) FROM "user"; -- must ERROR with 42501 (permission denied)
RESET ROLE;</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 25 · DDB STREAMS ======================================== -->
<!-- =============================================================== -->
<section id="ddb-streams">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 25</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">DDB Streams + PITR + deletion protection</h2>
</div>
<div class="review-block">
<h4>25.1 · Enable Streams before cutover <span class="sev crit">CRITICAL</span></h4>
<div class="label">CODE VALIDATION</div>
<p>No Streams configured today. Phase 4 cutover plan requires them.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>for (const DDBtable of Object.values(amplifyDynamoDbTables)) {
DDBtable.deletionProtectionEnabled = env === 'prod';
DDBtable.pointInTimeRecoveryEnabled = true;
DDBtable.streamSpecification = {
streamViewType: 'NEW_AND_OLD_IMAGES',
};
}</code></pre>
<div class="label ok">VERIFICATION</div>
<pre class="sh"><code>for table in $(aws dynamodb list-tables --query "TableNames[]" --output text); do
echo -n "$table: "
aws dynamodb describe-table --table-name "$table" --query "Table.StreamSpecification.StreamEnabled" --output text
done
# All must print "True"</code></pre>
</div>
<div class="review-block">
<h4>25.2 · Deletion protection — currently OFF <span class="sev crit">CRITICAL</span></h4>
<p>5-minute fix. <code>backend.ts:180</code>: <code>deletionProtectionEnabled = false</code>. Flip env-aware (false in dev-test, true in staging + prod).</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 26 · COGNITO ============================================ -->
<!-- =============================================================== -->
<section id="cognito">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 26</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Cognito — the coordination problem</h2>
</div>
<div class="review-block">
<h4>26.1 · The external-pool reality <span class="sev crit">CRITICAL</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify/auth/resource.ts</code> uses <code>referenceAuth()</code> with env-driven pool IDs. Amplify does NOT own the pools.</p>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>Pool-owner is a person Mike has to coordinate with. Required changes per pool × 3 envs:
<ol>
<li>Add custom attribute <code>custom:trustCompanyId</code> (string, immutable).</li>
<li>Add custom attribute <code>custom:onboardingCompleted</code> (string "true"/"false", mutable).</li>
<li>Confirm password policy meets HIPAA: min 12 chars, mixed case, numbers, symbols.</li>
<li>Confirm access token TTL (1 hr OK; 24 hr is too long).</li>
<li>Confirm advanced security features (compromised credentials, adaptive auth).</li>
</ol>
</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Find the pool owner.</li>
<li>Coordinate add of 2 custom attributes per pool.</li>
<li>Get pool config (password policy, TTLs, advanced security) into audit-archive S3.</li>
<li>Write + run backfill script per env:
<pre class="ts"><code>// scripts/backfill-cognito-tenant.ts
import { CognitoIdentityProviderClient, ListUsersCommand, AdminUpdateUserAttributesCommand }
from '@aws-sdk/client-cognito-identity-provider';
const SEED_TENANT = '00000000-0000-4000-8000-000000000001';
const c = new CognitoIdentityProviderClient({});
let token: string | undefined;
let processed = 0;
do {
const list = await c.send(new ListUsersCommand({
UserPoolId: process.env.USER_POOL_ID!,
Limit: 60, PaginationToken: token,
}));
for (const u of list.Users ?? []) {
const hasAttr = u.Attributes?.some(a =&gt; a.Name === 'custom:trustCompanyId');
if (hasAttr) continue;
await c.send(new AdminUpdateUserAttributesCommand({
UserPoolId: process.env.USER_POOL_ID!,
Username: u.Username!,
UserAttributes: [{ Name: 'custom:trustCompanyId', Value: SEED_TENANT }],
}));
processed++;
}
token = list.PaginationToken;
} while (token);
console.log(`Backfilled ${processed} users`);</code></pre>
</li>
<li>Run backfill on each of 3 envs <strong>before</strong> shipping fail-closed preTokenGen change.</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p><code>aws cognito-idp list-users --user-pool-id ... --query "Users[?!Attributes[?Name=='custom:trustCompanyId']]"</code> returns empty.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 27 · CUSTOM AUTH ======================================== -->
<!-- =============================================================== -->
<section id="customauth">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 27</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">customAuth Lambda — from stub to gate</h2>
</div>
<div class="review-block">
<h4>27.1 · Fill in the passthrough stub <span class="sev high">HIGH</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify/functions/custom-auth/handler.ts</code> currently returns <code>{ isAuthorized: true }</code> unconditionally.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>import type { AppSyncAuthorizerHandler } from 'aws-lambda';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.USER_POOL_ID!,
tokenUse: 'access',
clientId: process.env.USER_POOL_CLIENT_ID!,
});
type ResolverContext = {
trustCompanyId: string;
externalId: string;
groups: string;
};
export const handler: AppSyncAuthorizerHandler&lt;ResolverContext&gt; = async event =&gt; {
try {
const token = event.authorizationToken;
if (!token) return { isAuthorized: false };
const claims = await verifier.verify(token);
const trustCompanyId = claims['trustCompanyId'] as string | undefined;
const externalId = claims['externalId'] as string | undefined;
const groups = (claims['cognito:groups'] as string[] | undefined) ?? [];
if (!trustCompanyId) return { isAuthorized: false };
if (!externalId) return { isAuthorized: false };
return {
isAuthorized: true,
resolverContext: { trustCompanyId, externalId, groups: groups.join(',') },
ttlOverride: 300,
};
} catch (err) {
console.log('customAuth denied:', (err as Error).message);
return { isAuthorized: false };
}
};</code></pre>
<div class="label">env vars to add in backend.ts</div>
<pre class="ts"><code>backend.customAuth.addEnvironment('USER_POOL_ID', process.env.CFLegacy_UserPoolID!);
backend.customAuth.addEnvironment('USER_POOL_CLIENT_ID', process.env.CFLegacy_userPoolClientID!);</code></pre>
<div class="label ok">VERIFICATION</div>
<ul>
<li>Forged JWT (different signing key) → 401.</li>
<li>Expired JWT → 401.</li>
<li>Valid JWT missing trustCompanyId → 401.</li>
<li>Valid full JWT → 200 with response.</li>
</ul>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 28 · LAMBDA VPC ========================================= -->
<!-- =============================================================== -->
<section id="lambda-vpc">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 28</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Lambda VPC attachment — only 4 of 74</h2>
</div>
<div class="review-block">
<h4>28.1 · Which 4 Lambdas go in VPC <span class="sev crit">CRITICAL · Phase 3</span></h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>Out of <strong>74 Lambdas</strong>, only those that need direct Aurora connection:
<ol>
<li><strong>The dispatcher Lambda</strong> (new) — connects to RDS Proxy.</li>
<li><strong>encrypt-field</strong> (new) — RDS Proxy to look up authorization context.</li>
<li><strong>decrypt-field</strong> (new) — same.</li>
<li><strong>Outbox worker Lambda</strong> (new) — reads from Aurora outbox table.</li>
</ol>
<strong>That's 4, not 74.</strong>
</li>
<li>4 × 50 concurrent = 200 ENIs — within 250 soft limit.</li>
<li>The remaining 70 Lambdas stay non-VPC, talk to AppSync over HTTPS.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>backend.dataLayer.resources.lambda.connections.allowToDefaultPort(rdsProxy);
backend.dataLayer.resources.cfnResources.cfnFunction.vpcConfig = {
subnetIds: vpc.privateSubnets.map(s =&gt; s.subnetId),
securityGroupIds: [dispatcherSecurityGroup.securityGroupId],
};
backend.dataLayer.resources.cfnResources.cfnFunction.reservedConcurrentExecutions = 100;
// Repeat for encryptField, decryptField, outboxWorker</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 29 · PC ================================================= -->
<!-- =============================================================== -->
<section id="pc">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 29</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Provisioned Concurrency — 5 hot Lambdas</h2>
</div>
<div class="review-block">
<h4>29.1 · The hot-path Lambdas <span class="sev high">HIGH</span></h4>
<table>
<thead><tr><th>Lambda</th><th>Why PC</th><th>Baseline</th><th>Cost/mo</th></tr></thead>
<tbody>
<tr><td><code>dataLayer</code> (dispatcher)</td><td>Sees 100% of PHI traffic; cold start = visible UX hit</td><td>5 (autoscale to 20)</td><td>~$23</td></tr>
<tr><td><code>preTokenGeneration</code></td><td>Cognito 5 s budget; every token mint</td><td>3</td><td>~$14</td></tr>
<tr><td><code>processWebhookV2</code> (Stripe)</td><td>Stripe retries at &gt; 10 s</td><td>2</td><td>~$9</td></tr>
<tr><td><code>postSignUpTrigger</code></td><td>5 s timeout; cold start = signup failure</td><td>2</td><td>~$9</td></tr>
<tr><td><code>custom-auth</code></td><td>On the critical path for every AppSync call once Lambda mode is the gate</td><td>3</td><td>~$14</td></tr>
<tr style="background:var(--accent-soft);font-weight:700;"><td><strong>PC subtotal</strong></td><td>5 Lambdas</td><td>15 baseline</td><td><strong>~$69/mo</strong></td></tr>
</tbody>
</table>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>backend.dataLayer.resources.cfnResources.cfnFunction.addPropertyOverride(
'ProvisionedConcurrencyConfig', { ProvisionedConcurrentExecutions: 5 }
);</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 30 · COLD START ========================================= -->
<!-- =============================================================== -->
<section id="cold-start">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 30</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Cold-start mitigation beyond PC</h2>
</div>
<div class="review-block">
<h4>30.1 · Lazy-import Sentry (50 Lambdas) <span class="sev high">HIGH</span></h4>
<div class="label">CODE VALIDATION</div>
<p>50 handlers import Sentry at module scope; <code>Sentry.init</code> runs on every cold start.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>// Pattern: lazy import only on error path; ~200 ms cold-start savings
let Sentry: typeof import('@sentry/aws-serverless') | null = null;
async function reportError(err: Error) {
if (!Sentry) {
Sentry = await import('@sentry/aws-serverless');
Sentry.init({
dsn: process.env.SENTRY_DSN!,
environment: process.env.SENTRY_ENV,
tracesSampleRate: 0.1,
enabled: ['production','staging','test'].includes(process.env.SENTRY_ENV!),
beforeSend(event) { return scrubPhi(event); },
});
}
Sentry.captureException(err);
}
export const handler = async (event) =&gt; {
try {
// ... handler logic ...
} catch (err) {
await reportError(err as Error);
throw err;
}
};</code></pre>
<div class="label ok">VERIFICATION</div>
<p>CloudWatch Lambda Insights <code>init_duration</code> P95 should drop by ~200 ms after this refactor lands.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 31 · SECRETS ============================================ -->
<!-- =============================================================== -->
<section id="secrets">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 31</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Secrets Manager + Amplify <code>secret()</code></h2>
</div>
<div class="review-block">
<h4>31.1 · Current secret inventory <span class="sev verified">VERIFIED</span></h4>
<div class="label">CODE VALIDATION</div>
<ul>
<li>STRIPE_SECRET_KEY (Stripe webhook + admin-activate-subscription)</li>
<li>STRIPE_WEBHOOK_SECRET (Stripe webhook)</li>
<li>MAILCHIMP_API_KEY + MAILCHIMP_SERVER_PREFIX + MAILCHIMP_LIST_ID (Stripe webhook + processSignUp)</li>
<li>UPS_CLIENT_SECRET (create-label + track-package + generateShippingLabel)</li>
<li>GOOGLE_API_KEY + GOOGLE_API_SECRET (findAddress + getPlaceDetails + validateAddress)</li>
<li>FAX_CLIENT_ID + FAX_CLIENT_SECRET + FAX_JWT_TOKEN (sendFax)</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<ol style="margin:6px 0 0;">
<li>Encrypt all Amplify secrets with <code>chil-secrets-cmk</code> CMK.</li>
<li>Quarterly rotation for STRIPE_SECRET_KEY, MAILCHIMP_API_KEY, UPS_CLIENT_SECRET, GOOGLE_API_KEY, FAX_CLIENT_SECRET.</li>
<li>Document rotation procedure (where each vendor lets you rotate).</li>
</ol>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 32 · S3 PATHS =========================================== -->
<!-- =============================================================== -->
<section id="s3-paths">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 32</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">S3 path policies — extend with tenant prefix</h2>
</div>
<div class="review-block">
<h4>32.1 · 6 prefixes need <code>tenants/{trustCompanyId}/</code> wrapping <span class="sev high">HIGH · Phase 3/4</span></h4>
<div class="label">CODE VALIDATION</div>
<pre class="ts"><code>'public/evidence/*' // ADMIN, WELONTRUST, owner
'public/documents/*' // ADMIN, WELONTRUST, owner
'public/care-documents/{identity}/*' // ADMIN, owner
'public/personal-id/*' // ADMIN, WELONTRUST, owner
'public/support-requests/{identity}/*' // ADMIN, owner
'public/template-sections/*' // ADMIN</code></pre>
<div class="label crit">DEEP THINKING</div>
<p>Amplify storage access rules can't directly read a custom JWT claim into a path token. The <code>{trustCompanyId}</code> segment must be enforced by upload/download broker Lambdas that build the key from the verified tenant claim, never from client input.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>// Upload broker (new) builds key from VERIFIED claims
export async function POST(req: Request) {
const { trustCompanyId, externalId } = await getServerSideJwtClaims();
const { fileName, contentType } = await req.json();
if (!/^[\w\-.]+$/.test(fileName)) return new Response('bad filename', { status: 400 });
const key = `tenants/${trustCompanyId}/documents/${externalId}/${Date.now()}-${fileName}`;
const client = new S3Client({});
const url = await getSignedUrl(client, new PutObjectCommand({
Bucket: process.env.STORAGE_BUCKET!,
Key: key,
ContentType: contentType,
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: process.env.S3_PHI_KMS_KEY_ARN!,
}), { expiresIn: 300 });
return Response.json({ url, key });
}</code></pre>
<div class="label ok">VERIFICATION</div>
<p>From a tenant-A user, attempt to upload to <code>tenants/&lt;tenant-B-id&gt;/...</code> via crafted PUT — must fail.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 33 · AUDIT BUCKET ======================================= -->
<!-- =============================================================== -->
<section id="audit-bucket">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 33</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">S3 audit-archive — Object Lock Compliance mode</h2>
</div>
<div class="review-block">
<h4>33.1 · 7-year Compliance lock <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>HIPAA requires 6-year retention. Use Object Lock Compliance mode (strictly immutable; root account can't delete).</li>
<li>Compliance mode vs Governance mode: <strong>Compliance</strong>.</li>
<li>Retention 7 years (1 year safety margin).</li>
<li>Glacier transition after 180 days; Deep Archive after 730 days.</li>
<li>Versioning enabled (required for Object Lock).</li>
<li>KMS: dedicated chil-audit-archive CMK; deny encryption by application roles.</li>
<li>Block all public access — no exceptions.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const auditBucket = new s3.Bucket(stack, `AuditArchive-${env}`, {
bucketName: `chil-audit-archive-${env}-${account}`,
encryption: s3.BucketEncryption.KMS,
encryptionKey: auditKey,
versioned: true,
objectLockEnabled: true,
objectLockDefaultRetention: s3.ObjectLockRetention.compliance({
duration: cdk.Duration.days(365 * 7),
}),
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
lifecycleRules: [{
id: 'AuditArchiveGlacier',
transitions: [
{ storageClass: s3.StorageClass.GLACIER_INSTANT_RETRIEVAL, transitionAfter: cdk.Duration.days(180) },
{ storageClass: s3.StorageClass.DEEP_ARCHIVE, transitionAfter: cdk.Duration.days(730) },
],
}],
removalPolicy: cdk.RemovalPolicy.RETAIN,
});</code></pre>
<div class="label ok">VERIFICATION</div>
<p><code>aws s3api get-object-lock-configuration --bucket chil-audit-archive-prod-...</code> returns Mode=COMPLIANCE, Days=2555. Attempt <code>aws s3 rm</code> — must fail with AccessDenied.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 34 · CLOUDTRAIL ========================================= -->
<!-- =============================================================== -->
<section id="cloudtrail">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 34</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">CloudTrail — org trail + S3 data events</h2>
</div>
<div class="review-block">
<h4>34.1 · Organization trail with PHI bucket data events <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label crit">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>Management Events always on (free). Data Events (S3 GetObject/PutObject, KMS Decrypt) are charged but mandatory for HIPAA.</li>
<li>Org Trail aggregates across accounts. Single-account: account trail.</li>
<li>Log File Validation — hourly hash-signed log files; auditors love this.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const trail = new cloudtrail.Trail(stack, 'OrgTrail', {
bucket: auditBucket,
s3KeyPrefix: 'cloudtrail/',
includeGlobalServiceEvents: true,
isMultiRegionTrail: true,
enableFileValidation: true,
encryptionKey: auditKey,
});
trail.addS3EventSelector([
{ bucket: backend.storage.resources.bucket },
{ bucket: backend.storage.resources.bucket, objectPrefix: 'tenants/' },
], { readWriteType: cloudtrail.ReadWriteType.ALL });</code></pre>
<div class="label ok">VERIFICATION</div>
<p>Test S3 PutObject; within 15 min audit-archive bucket has CloudTrail log showing the API call. <code>aws cloudtrail validate-logs</code> returns OK.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 35 · WAF ================================================ -->
<!-- =============================================================== -->
<section id="waf">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 35</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">WAF v2 in front of AppSync</h2>
</div>
<div class="review-block">
<h4>35.1 · WAF Web ACL — OWASP + rate limit + optional geo-block <span class="sev high">HIGH · Phase 1</span></h4>
<div class="label">CODE VALIDATION</div>
<p>No WAF in <code>backend.ts</code> today.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const webAcl = new wafv2.CfnWebACL(stack, `WebAcl-${env}`, {
scope: 'REGIONAL',
defaultAction: { allow: {} },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'chil-waf' },
rules: [
{
name: 'AWS-AWSManagedRulesCommonRuleSet', priority: 1,
overrideAction: { none: {} },
statement: { managedRuleGroupStatement: { vendorName: 'AWS', name: 'AWSManagedRulesCommonRuleSet' } },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'AWSManagedRulesCommonRuleSetMetric' },
},
{
name: 'RateLimit', priority: 2,
action: { block: {} },
statement: { rateBasedStatement: { limit: 2000, aggregateKeyType: 'IP' } },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'RateLimit' },
},
],
});
new wafv2.CfnWebACLAssociation(stack, 'AppSyncWafAssociation', {
resourceArn: backend.data.resources.graphqlApi.arn,
webAclArn: webAcl.attrArn,
});</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 36 · APPSYNC ============================================ -->
<!-- =============================================================== -->
<section id="appsync">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 36</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">AppSync hardening</h2>
</div>
<div class="review-block">
<h4>36.1 · Disable introspection + depth limit + access logs <span class="sev high">HIGH</span></h4>
<div class="label">CODE VALIDATION</div>
<p>Currently nothing — <code>data/resource.ts</code> has no <code>introspection</code> or <code>maxDepth</code>.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const cfnGraphQLApi = backend.data.resources.cfnResources.cfnGraphqlApi;
cfnGraphQLApi.introspectionConfig = env === 'prod' ? 'DISABLED' : 'ENABLED';
cfnGraphQLApi.queryDepthLimit = 10;
cfnGraphQLApi.resolverCountLimit = 75;
cfnGraphQLApi.logConfig = {
fieldLogLevel: 'ERROR',
excludeVerboseContent: true,
cloudWatchLogsRoleArn: appSyncLogRole.roleArn,
};</code></pre>
<div class="label ok">VERIFICATION</div>
<p>Unauth'd <code>POST /graphql {"query": "{ __schema { types { name } } }"}</code> — must 400 in prod.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 37 · CSP ================================================ -->
<!-- =============================================================== -->
<section id="csp">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 37</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">CSP + HTTP headers (already strong)</h2>
</div>
<div class="review-block">
<h4>37.1 · Preserve existing implementation <span class="sev verified">VERIFIED</span></h4>
<p><code>middleware.ts:1-50</code> implements strong CSP with per-request nonce. <code>next.config.mjs</code> adds HSTS (1 year), X-Content-Type-Options, X-Frame-Options DENY, X-XSS-Protection, Referrer-Policy.</p>
<div class="label">DEVOPS DELIVERABLE</div>
<p>None — preserve. Test via <code>https://securityheaders.com</code>; should score A+.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 38 · CI/CD ============================================== -->
<!-- =============================================================== -->
<section id="cicd">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 38</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">CI/CD security gates</h2>
</div>
<div class="review-block">
<h4>38.1 · SAST, SCA, secret-scan, IaC scan <span class="sev high">HIGH · Phase 1</span></h4>
<div class="label">CODE VALIDATION</div>
<p><code>amplify.yml</code> has no security gates today.</p>
<div class="label crit">DEEP THINKING</div>
<p>SOC 2 CC8 requires every change pass automated controls. Tools:</p>
<ul style="margin:6px 0 0;">
<li><strong>Semgrep</strong> (SAST) — rules for TS/Node; free Community tier</li>
<li><strong>pnpm audit</strong> (SCA, dependency vulns) — free, built in</li>
<li><strong>Snyk</strong> (deeper SCA + license check) — $50/dev/mo or free OSS</li>
<li><strong>TruffleHog</strong> (secret-scan) — free; catches AWS/Stripe/etc. keys</li>
<li><strong>checkov</strong> (IaC for CDK) — free</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="yaml"><code>backend:
phases:
preBuild:
commands:
- npm install -g pnpm@10.33.4 semgrep
- echo "Running SAST..."
- semgrep --config p/typescript --error --strict
- echo "Running SCA..."
- pnpm audit --audit-level=high
- echo "Running secret scan..."
- npx -y trufflehog filesystem . --no-update --fail
- echo "Running IaC scan..."
- pip install checkov && checkov -d amplify/ --quiet --soft-fail-on LOW
build:
commands:
# ... existing build</code></pre>
<div class="label ok">VERIFICATION</div>
<p>Push a PR with a hardcoded API key — CI must fail at secret-scan. PR with a vulnerable dependency — CI must fail at pnpm audit.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 39 · DRIZZLE RUNNER ===================================== -->
<!-- =============================================================== -->
<section id="drizzle-runner">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 39</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Migration runner — Drizzle Kit + CI integration</h2>
</div>
<div class="review-block">
<h4>39.1 · CI applies migrations on deploy <span class="sev high">HIGH · Phase 1</span></h4>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="yaml"><code># amplify.yml (in backend phase)
- echo "Running migration dry-run in throwaway container..."
- docker run -d --name pg-test -e POSTGRES_PASSWORD=test -p 5433:5432 postgres:16-alpine
- sleep 3
- DATABASE_URL=postgresql://postgres:test@localhost:5433/postgres npx drizzle-kit migrate
- docker stop pg-test && docker rm pg-test
# Actual deploy:
- export DB_HOST=$(aws ssm get-parameter --name /chil/${ENV}/db-host --query "Parameter.Value" --output text)
- export DB_TOKEN=$(aws rds generate-db-auth-token --hostname $DB_HOST --port 5432 --username chil_migrator)
- DATABASE_URL="postgresql://chil_migrator:${DB_TOKEN}@${DB_HOST}:5432/chil?sslmode=require" npx drizzle-kit migrate</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 40 · YAML BUG =========================================== -->
<!-- =============================================================== -->
<section id="yaml-bug">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 40</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">amplify.yml fixes (the smart-quote bug)</h2>
</div>
<p>5-minute fix per D1:</p>
<pre class="sh"><code>sed -i '' 's/“/"/g; s/”/"/g' amplify.yml
git diff amplify.yml
git commit -m "Fix smart-quote bug in amplify.yml"</code></pre>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 41 · DASHBOARDS ========================================= -->
<!-- =============================================================== -->
<section id="dashboards">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 41</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">CloudWatch dashboards — 3 panels</h2>
</div>
<div class="review-block">
<h4>41.1 · Data plane / Request plane / Compliance plane <span class="sev high">HIGH · Phase 1</span></h4>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>const dataPlane = new cw.Dashboard(stack, `DataPlane-${env}`, {
dashboardName: `chil-data-plane-${env}`,
});
dataPlane.addWidgets(
new cw.GraphWidget({ title: 'Dispatcher Invocations',
left: [backend.dataLayer.resources.lambda.metricInvocations()] }),
new cw.GraphWidget({ title: 'Dispatcher Duration P95',
left: [backend.dataLayer.resources.lambda.metricDuration({ statistic: 'p95' })] }),
new cw.GraphWidget({ title: 'Aurora ACUs',
left: [new cw.Metric({ namespace: 'AWS/RDS', metricName: 'ServerlessDatabaseCapacity',
dimensionsMap: { DBClusterIdentifier: cluster.clusterIdentifier } })] }),
new cw.GraphWidget({ title: 'Aurora Connections',
left: [new cw.Metric({ namespace: 'AWS/RDS', metricName: 'DatabaseConnections',
dimensionsMap: { DBClusterIdentifier: cluster.clusterIdentifier } })] }),
);
// Repeat for requestPlane (AppSync) and compliancePlane (pgaudit, CloudTrail events)</code></pre>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 42 · ALARMS ============================================= -->
<!-- =============================================================== -->
<section id="alarms">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 42</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Alarms inventory — what pages on-call</h2>
</div>
<table>
<thead><tr><th>Alarm</th><th>Metric</th><th>Threshold</th><th>Severity</th></tr></thead>
<tbody>
<tr><td>RLS denial rate</td><td>Sentry "RLS denial" OR CloudWatch on SQLSTATE 42501 from dispatcher logs</td><td>&gt; 0.1% sustained 5 min</td><td>Page</td></tr>
<tr><td>Tenant claim missing</td><td>Sentry "missing trustCompanyId"</td><td>Any</td><td>Page</td></tr>
<tr><td>preTokenGen error rate</td><td>Lambda <code>Errors</code></td><td>&gt; 0.5%</td><td>Page</td></tr>
<tr><td>Cognito 5 s timeout near miss</td><td>preTokenGen <code>Duration</code> P95</td><td>&gt; 4000 ms sustained 5 min</td><td>Alert</td></tr>
<tr><td>Dispatcher error rate</td><td>Lambda <code>Errors</code></td><td>&gt; 1% sustained 5 min</td><td>Page</td></tr>
<tr><td>Aurora ACU saturation</td><td><code>ServerlessDatabaseCapacity</code></td><td>at max for 10 min</td><td>Alert</td></tr>
<tr><td>Aurora connection saturation</td><td><code>DatabaseConnections</code></td><td>&gt; 80% of max</td><td>Alert</td></tr>
<tr><td>RDS Proxy auth failures</td><td><code>ClientAuthenticationFailures</code></td><td>&gt; 5 in 5 min</td><td>Page</td></tr>
<tr><td>Stripe webhook 5xx rate</td><td>processWebhookV2 <code>Errors</code></td><td>&gt; 2% sustained</td><td>Page</td></tr>
<tr><td>Lambda init duration P95</td><td>Lambda Insights <code>init_duration</code></td><td>&gt; 2000 ms</td><td>Alert</td></tr>
<tr><td>WAF block rate spike</td><td>WAF <code>BlockedRequests</code></td><td>&gt; 100 / min</td><td>Alert</td></tr>
<tr><td>CloudTrail log file integrity failure</td><td>CloudTrail <code>LogFileValidationFailure</code></td><td>Any</td><td>Page</td></tr>
<tr><td>chil_admin (break-glass) used</td><td>CloudWatch Insights on pgaudit for chil_admin sessions</td><td>Any non-scheduled use</td><td>Alert</td></tr>
<tr><td>Cost anomaly</td><td>AWS Cost Anomaly Detection</td><td>Configured per service</td><td>Alert</td></tr>
<tr><td>Backup failure</td><td>RDS BackupRetentionPeriod / snapshot creation missing</td><td>Daily check</td><td>Alert</td></tr>
<tr><td>15-min Lambdas near timeout</td><td>Per-function <code>Duration</code> (5 Lambdas at 900s)</td><td>&gt; 720000 ms</td><td>Page</td></tr>
</tbody>
</table>
<p><strong>Severity convention:</strong> Page = wake on-call (PagerDuty). Alert = Slack + email.</p>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 43 · SENTRY ============================================= -->
<!-- =============================================================== -->
<section id="sentry">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 43</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Sentry hardening</h2>
</div>
<div class="review-block">
<h4>43.1 · Three config files, three gaps <span class="sev crit">CRITICAL · Phase 1</span></h4>
<div class="label">CODE VALIDATION</div>
<ul>
<li><code>sentry.server.config.ts</code>: <code>tracesSampleRate: 1</code>, no <code>sendDefaultPii</code>, no <code>beforeSend</code>, <code>enableLogs: true</code>.</li>
<li><code>sentry.edge.config.ts</code>: same.</li>
<li><code>instrumentation-client.ts</code>: <code>sendDefaultPii: false</code> ✓, but <code>tracesSampleRate: 1</code>.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<pre class="ts"><code>// sentry.server.config.ts AND sentry.edge.config.ts
import * as Sentry from '@sentry/nextjs';
const PHI_KEYS = new Set([
'email','phone','phoneNumber','firstName','lastName','middleName',
'birthdate','dateOfBirth','clientDateOfBirth','ssn','address',
'addressLine1','addressLine2','medicalNotes','medicalConditions',
'hospitalName','hospitalAddress','hospitalFax','hospitalEmail',
'contactPhone','contactFirstName','contactLastName',
]);
function scrub(value: unknown): unknown {
if (value == null || typeof value === 'string') return value;
if (Array.isArray(value)) return value.map(scrub);
if (typeof value === 'object') {
const out: Record&lt;string, unknown&gt; = {};
for (const [k, v] of Object.entries(value as object)) {
out[k] = PHI_KEYS.has(k) ? '[REDACTED]' : scrub(v);
}
return out;
}
return value;
}
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.SENTRY_ENV ?? 'development',
enabled: ['production','staging','test'].includes(process.env.SENTRY_ENV ?? ''),
tracesSampleRate: 0.1,
sendDefaultPii: false,
enableLogs: false,
beforeSend(event) {
if (event.extra) event.extra = scrub(event.extra) as typeof event.extra;
if (event.contexts) event.contexts = scrub(event.contexts) as typeof event.contexts;
if (event.request?.data) event.request.data = scrub(event.request.data);
event.breadcrumbs?.forEach(b =&gt; { if (b.data) b.data = scrub(b.data) as typeof b.data; });
return event;
},
beforeBreadcrumb(crumb) {
if (crumb.data) crumb.data = scrub(crumb.data) as typeof crumb.data;
return crumb;
},
});</code></pre>
<div class="label ok">VERIFICATION</div>
<p>Trigger an exception with PHI in context — Sentry event shows <code>[REDACTED]</code>.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 44 · BACKUPS ============================================ -->
<!-- =============================================================== -->
<section id="backups">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 44</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Backup strategy</h2>
</div>
<table>
<thead><tr><th>Resource</th><th>Hot retention</th><th>Long-tail</th></tr></thead>
<tbody>
<tr><td>Aurora cluster</td><td>30 days continuous PITR</td><td>Daily snapshot → S3 audit-archive 6+ years</td></tr>
<tr><td>Aurora reader</td><td>Same as cluster (shared storage)</td><td>—</td></tr>
<tr><td>DDB tables (5 remaining)</td><td>35-day PITR</td><td>AWS Backup → S3 6+ years</td></tr>
<tr><td>S3 PHI buckets</td><td>Versioning enabled</td><td>Glacier 180d; Deep Archive 730d</td></tr>
<tr><td>S3 audit-archive</td><td>Object Lock 7-year</td><td>Same bucket; lifecycle to Deep Archive 730d</td></tr>
<tr><td>Secrets Manager</td><td>Versioning (default)</td><td>—</td></tr>
</tbody>
</table>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 45 · DR DRILL =========================================== -->
<!-- =============================================================== -->
<section id="dr-drill">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 45</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">DR drill mechanics</h2>
</div>
<div class="review-block">
<h4>45.1 · Quarterly drill procedure <span class="sev high">HIGH · Phase 5 + recurring</span></h4>
<ol style="margin:6px 0 0;">
<li>Pick PITR T (e.g. T−1 h).</li>
<li>Restore Aurora to parallel cluster in sibling VPC.</li>
<li>Shadow AppSync at <code>dr.chil.welon</code>.</li>
<li>Clone dispatcher Lambda pointing at restored Aurora.</li>
<li>Run readiness probe battery (row counts ±1%, RLS tests, 25-query fidelity, cold-start P95 &lt; 3 s).</li>
<li>Record RTO + RPO.</li>
<li>File report in audit-archive S3 with date.</li>
<li>Tear down shadow.</li>
</ol>
<div class="label ok">VERIFICATION</div>
<p>Quarterly drill report shows consistent RTO &lt; 4 h, RPO &lt; 5 min over rolling 4 quarters.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 46 · CUTOVER INFRA ====================================== -->
<!-- =============================================================== -->
<section id="cutover-infra">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 46</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Cutover-temporary infrastructure</h2>
</div>
<table>
<thead><tr><th>Item</th><th>When provisioned</th><th>When torn down</th></tr></thead>
<tbody>
<tr><td>DynamoDB Streams</td><td>T−1 week</td><td>After 30-day stability post-cutover</td></tr>
<tr><td>CDC catch-up Lambda</td><td>T−1 week</td><td>After 30-day stability</td></tr>
<tr><td>S3 PITR export staging bucket</td><td>T−2 days</td><td>After cutover + retention window</td></tr>
<tr><td>ETL Lambdas (S3 → Aurora COPY)</td><td>T−1 week</td><td>After cutover successful</td></tr>
<tr><td>Maintenance-mode middleware toggle</td><td>T−1 day</td><td>T+3:00</td></tr>
<tr><td>Stripe webhook temp-503 route</td><td>T−15 min</td><td>T+3:00</td></tr>
<tr><td>Shadow AppSync for smoke</td><td>T−1 week</td><td>T+24 h after stability</td></tr>
</tbody>
</table>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 47 · ROLLBACK =========================================== -->
<!-- =============================================================== -->
<section id="rollback">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 47</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Rollback mechanics</h2>
</div>
<div class="review-block">
<h4>47.1 · The 4-hour rollback path</h4>
<div class="label">DEEP THINKING</div>
<ul style="margin:6px 0 0;">
<li>Rollback = flip the CFN parameter routing PHI resolvers from dispatcher back to DDB. <strong>Single parameter change.</strong></li>
<li>Frontend never changed; rollback invisible to clients except brief 503 during maintenance-mode.</li>
<li>"Leave DDB hot for cutover weekend" guarantee makes 4 h achievable. PITR alone cannot restore 41 tables in 4 hours.</li>
<li>Data delta: writes between T+2:30 and rollback time live only in Aurora. Replay these into DDB via one-shot Lambda before declaring rollback complete.</li>
</ul>
<div class="label">DEVOPS DELIVERABLE</div>
<p>Pre-write rollback runbook with exact CLI commands. Print it. Tape it to Mike's wall.</p>
</div>
</section>
<hr class="devops">
<!-- =============================================================== -->
<!-- ===== 48 · DECOMMISSION ======================================= -->
<!-- =============================================================== -->
<section id="decommission">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">DevOps · 48</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Decommission plan (T+30 days)</h2>
</div>
<ol>
<li>Confirm zero traffic to old PHI DDB tables for 30 consecutive days.</li>
<li>Final snapshot of each old DDB table; copy to S3 audit-archive with 7-year retention.</li>
<li>Delete DDB tables (flip deletion protection off first, with explicit audit-log entry).</li>
<li>Tear down cutover-temp infrastructure.</li>
<li>Update DevOps runbook.</li>
</ol>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 6 of 8</div>
<h2 style="border:0;color:var(--ink);">Frontend — exact deltas, no surprises</h2>
<p>The frontend is NOT rewritten. <code>generateClient&lt;Schema&gt;()</code> works. The changes below are mechanical — mostly renames or shifts from JSON-blob reads to typed child-table reads. ~45–65 files touched.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 49 · FRONTEND CATALOG =================================== -->
<!-- =============================================================== -->
<section id="frontend-catalog">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 6 · 49</span>
<h2 style="border:0;margin:18px 0 0;">Frontend change catalog</h2>
</div>
<table>
<thead><tr><th>Change</th><th>Files</th><th>Effort</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>Rename <code>user.cognitoId</code> → <code>user.externalId</code></td><td>~5–10</td><td>Search-and-replace + grep verification</td><td>3</td></tr>
<tr><td>Status field collapse: <code>status</code>/<code>membershipStatus</code>/<code>paymentsStatus</code> → <code>lifecycle_state</code> + <code>billing_state</code></td><td>~15–25</td><td>Mechanical; map enum values; one PR per surface area</td><td>3</td></tr>
<tr><td>Move <code>user.interviewProgressV2</code> → <code>userInterview.stepsData</code></td><td>~3–5</td><td>Switch to <code>client.models.UserInterview.get({ userId })</code>. Remove JSON double-encoding workarounds.</td><td>3</td></tr>
<tr><td>Move <code>user.peopleLibrary</code> → <code>userPeopleLibrary.list({ filter: { userId } })</code></td><td>~3–5</td><td>Per-person row instead of JSON array</td><td>3</td></tr>
<tr><td>Move <code>user.quarterlyReviewData</code> → <code>userQuarterlyReview.list(...)</code></td><td>~2</td><td>Same pattern</td><td>3</td></tr>
<tr><td>Replace <code>user.documentationStatus</code> JSON → derive from <code>document.status</code></td><td>~5</td><td>Replace JSON parse with derive helper; loses double-encoding bug</td><td>3</td></tr>
<tr><td>Welon arrays → welon_assignment lookups</td><td>~10–15</td><td>Switch <code>user.assignedWelonTrustIds.includes(...)</code> → <code>welonAssignments.find(wa =&gt; ...)</code></td><td>3</td></tr>
<tr><td>Care document — one row per section</td><td>~10–15</td><td>16 section components: <code>client.models.CareDocumentSection.list({ filter: { careDocumentId, sectionType } })</code></td><td>3</td></tr>
<tr><td>Support thread — messages as child entities</td><td>~3</td><td>Switch from embedded array to <code>client.models.SupportMessage.list(...)</code></td><td>3</td></tr>
<tr><td>MEDICALREVIEW screens — caseload filter</td><td>~3</td><td>Server-enforced via RLS. UI may add "your caseload (N members)" hint.</td><td>3</td></tr>
<tr><td>MemberNote screens — welon-assignment filter</td><td>~3</td><td>Server-enforced. UI may show "assigned to you" label.</td><td>3</td></tr>
<tr><td>Step-up MFA challenge modal</td><td>~2 new</td><td>On <code>NeedsFreshMFA</code> error: modal, MFA code, retry mutation (~50 lines)</td><td>3</td></tr>
<tr><td>Field-encryption wrappers in Digital Assets</td><td>~2</td><td>Read: <code>decryptCredentialsMutation</code>. Write: <code>encryptCredentialsMutation</code>. React state stays plaintext.</td><td>3</td></tr>
<tr><td>JWT claim helper</td><td>~1 new</td><td><code>useTenantId()</code> hook reads <code>trustCompanyId</code> from access token</td><td>3</td></tr>
<tr><td>Pagination UX improvements (optional)</td><td>~5</td><td>Replace nextToken-based "infinite scroll until empty" with cursor pagination. Now predictable.</td><td>5</td></tr>
<tr><td>Reporting page (new)</td><td>~5 new</td><td>Replaces Apps Script prototype. Server components query dispatcher via AppSync. ~10 KPI queries.</td><td>5</td></tr>
</tbody>
</table>
<div class="callout ok">
<div class="callout-label">Net frontend impact</div>
<strong>~45–65 files touched.</strong> Mostly mechanical renames or shifts from JSON-blob reads to typed child-table
reads. <strong>Zero files require a "rewrite."</strong> Spread across Phase 3 (most) and Phase 5 (reporting page). No
frontend cutover needed at PHI cutover — frontend code is forward-compatible with both old and new schemas during the
transition.
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 50 · FRONTEND NO-OP ===================================== -->
<!-- =============================================================== -->
<section id="frontend-noop">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 6 · 50</span>
<h2 style="border:0;margin:18px 0 0;">What does NOT change in the frontend</h2>
</div>
<ul>
<li><strong>Authentication flow</strong> — Cognito sign-in, MFA, password reset all unchanged.</li>
<li><strong>CSP middleware with per-request nonce</strong> — unchanged.</li>
<li><strong>Sentry init</strong> — server-side scrubber/sample-rate are config-only; client init unchanged.</li>
<li><strong><code>generateClient&lt;Schema&gt;()</code></strong> — unchanged; <code>Schema</code> just has more honestly-named fields.</li>
<li><strong>Routing</strong> — <code>app/</code> structure unchanged.</li>
<li><strong>UI component library</strong> — unchanged.</li>
<li><strong>HTTP security headers</strong> — HSTS, X-Frame-Options, etc. unchanged.</li>
<li><strong>S3 upload paths</strong> — unchanged until multi-tenancy prefix lands in Phase 4/5.</li>
<li><strong>Stripe Elements, Botpress widget, Facebook SDK, Vimeo embeds</strong> — third-party integrations unchanged.</li>
</ul>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 7 of 8</div>
<h2 style="border:0;color:var(--ink);">Security &amp; compliance — every control mapped to a phase</h2>
<p>HIPAA Safeguards (§164.308/.310/.312) and SOC 2 Trust Service Criteria (CC1–CC9 + P/C) — which phase delivers each control. Plus the pre-cutover hard requirements and the auditor evidence pack assembled at Phase 5 close.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 51 · HIPAA ============================================== -->
<!-- =============================================================== -->
<section id="hipaa">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 7 · 51</span>
<h2 style="border:0;margin:18px 0 0;">HIPAA Safeguards — controls landing schedule</h2>
</div>
<h3>51.1 — Administrative Safeguards (§164.308)</h3>
<table>
<thead><tr><th>Citation</th><th>Standard</th><th>How</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>§164.308(a)(1)</td><td>Security Management Process — risk analysis</td><td>This + companion docs ARE the risk analysis; quarterly review thereafter</td><td>0; recurring</td></tr>
<tr><td>§164.308(a)(2)</td><td>Assigned Security Responsibility</td><td>Named Security Officer (Mike); Data Architect (Oleksii)</td><td>0</td></tr>
<tr><td>§164.308(a)(3)</td><td>Workforce Security</td><td>Cognito groups + canonical role enum + <code>deleteUserComplete</code> Lambda</td><td>1 (reaffirmed)</td></tr>
<tr><td>§164.308(a)(4)</td><td>Information Access Management — minimum necessary</td><td>Tenant boundary on every row; RLS policies; MEDICALREVIEW caseload; MemberNote welon-scoping; field-level encryption</td><td>2 + 3</td></tr>
<tr><td>§164.308(a)(5)</td><td>Security Awareness &amp; Training</td><td>HR/leadership-owned (out of scope here)</td><td>n/a</td></tr>
<tr><td>§164.308(a)(6)</td><td>Security Incident Procedures</td><td>Sentry + CloudWatch alarms + on-call + documented runbook</td><td>1 + ongoing</td></tr>
<tr><td>§164.308(a)(7)</td><td>Contingency Plan — backup, DR, testing</td><td>PITR enabled (Phase 1); cross-region snapshot copy (Phase 1); DR Drill performed + report filed (Phase 5); quarterly thereafter</td><td>1 + 5</td></tr>
<tr><td>§164.308(a)(8)</td><td>Evaluation — periodic</td><td>Annual pen-test (Phase 5 + recurring); quarterly architecture review</td><td>5 + recurring</td></tr>
<tr><td>§164.308(b)(1)</td><td>Business Associate Contracts (BAA)</td><td>AWS BAA confirmed; Sentry Enterprise BAA verified; Stripe BAA; Mailchimp BAA; UPS BAA verified</td><td>0</td></tr>
</tbody>
</table>
<h3>51.2 — Physical Safeguards (§164.310)</h3>
<table>
<thead><tr><th>Citation</th><th>Standard</th><th>How</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>§164.310(a)</td><td>Facility Access Controls</td><td>AWS data center controls (BAA-delegated)</td><td>0</td></tr>
<tr><td>§164.310(b)</td><td>Workstation Use</td><td>HR/IT policy</td><td>n/a</td></tr>
<tr><td>§164.310(c)</td><td>Workstation Security</td><td>MFA + FDE on workstations (IT-owned)</td><td>n/a</td></tr>
<tr><td>§164.310(d)</td><td>Device &amp; Media Controls — disposal, re-use, accountability, backup</td><td>AWS-managed; old DDB deleted after 30-day stability with audit retention</td><td>5</td></tr>
</tbody>
</table>
<h3>51.3 — Technical Safeguards (§164.312)</h3>
<table>
<thead><tr><th>Citation</th><th>Standard</th><th>How</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>§164.312(a)(1)</td><td>Access Control</td><td>Cognito + per-user JWT; chil_admin break-glass via temp creds; session timeouts; field-level encryption (Phase 3); RLS (Phase 2)</td><td>1, 2, 3</td></tr>
<tr><td>§164.312(a)(2)(i)</td><td>Unique User ID</td><td><code>external_id</code> + <code>cognito_sub</code> stored together (Phase 2 — fixes the misnaming); tenant claim adds tenant identity (Phase 1)</td><td>1 + 2</td></tr>
<tr><td>§164.312(a)(2)(ii)</td><td>Emergency Access</td><td><code>chil_admin</code> role with BYPASSRLS; IAM Identity Center + temp creds; every action audited via pgaudit</td><td>1</td></tr>
<tr><td>§164.312(a)(2)(iii)</td><td>Automatic Logoff</td><td>Cognito session timeouts (existing); customAuth Lambda fails closed (Phase 3)</td><td>1 + 3</td></tr>
<tr><td>§164.312(a)(2)(iv)</td><td>Encryption &amp; Decryption</td><td>CMK on Aurora + S3 + Secrets (Phase 1); Field-level encryption for highest-sensitivity PHI (Phase 3)</td><td>1 + 3</td></tr>
<tr><td>§164.312(b)</td><td>Audit Controls</td><td>CloudTrail (mgmt + data events) → S3 Object Lock; pgaudit → CloudWatch → S3; AppSync access logs; chil_audit / audit_event entity (Phase 2); Cognito events; 6+ year retention</td><td>1 + 2</td></tr>
<tr><td>§164.312(c)</td><td>Integrity</td><td>Append-only history tables (Phase 2); idempotent writes; transactional outbox writes in same transaction; document_hash on legal documents (existing)</td><td>2</td></tr>
<tr><td>§164.312(d)</td><td>Person/Entity Authentication</td><td>Cognito + email MFA (existing); IAM auth to Aurora via RDS Proxy (Phase 1); customAuth Lambda JWT verify (Phase 3); step-up MFA (Phase 3)</td><td>1 + 3</td></tr>
<tr><td>§164.312(e)</td><td>Transmission Security</td><td>TLS everywhere (existing); WAF v2; CloudFront TLS; <code>require_ssl=true</code> on Aurora (Phase 1)</td><td>1 (partial existing)</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 52 · SOC 2 ============================================== -->
<!-- =============================================================== -->
<section id="soc2">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 7 · 52</span>
<h2 style="border:0;margin:18px 0 0;">SOC 2 Trust Service Criteria — schedule</h2>
</div>
<table>
<thead><tr><th>Criterion</th><th>What it asks</th><th>How</th><th>Phase</th></tr></thead>
<tbody>
<tr><td>CC1 Control Environment</td><td>Commitment to security; competent personnel</td><td>Named Security Officer; Data Architect; branch protection + CI gates</td><td>0</td></tr>
<tr><td>CC2 Communication &amp; Information</td><td>Internal/external info quality</td><td>This document as architecture record; incident runbook; release log</td><td>0 + ongoing</td></tr>
<tr><td>CC3 Risk Assessment</td><td>Risk identification &amp; analysis</td><td>Companion doc § 9.2 gap inventory; recurring quarterly</td><td>0 + recurring</td></tr>
<tr><td>CC4 Monitoring Activities</td><td>Ongoing monitoring &amp; corrective action</td><td>3 CloudWatch dashboards (Phase 1); Sentry; cost &amp; usage alerts; per-phase verification gates</td><td>1 + recurring</td></tr>
<tr><td>CC5 Control Activities</td><td>Policies and procedures</td><td>§9.3 security principles in companion doc; CI enforces; PR review required</td><td>0 + 1+</td></tr>
<tr><td>CC6 Logical &amp; Physical Access Controls</td><td>Restrict to authorized users</td><td>Cognito + tenant claim + RLS (Phase 2) / customAuth gate (Phase 3); physical = AWS BAA</td><td>1, 2, 3</td></tr>
<tr><td>CC7 System Operations (incl. <strong>CC7.5</strong> recovery testing)</td><td>Issue detection; incident management; recovery plan tested</td><td>CloudWatch alarms (Phase 1); on-call; <strong>DR Drill quarterly with dated report (Phase 5 + recurring)</strong></td><td>1 + 5 + recurring</td></tr>
<tr><td>CC8 Change Management</td><td>Authorized changes</td><td>Branch protection; PR review; chil_migrator DDL-only role; CI security gates; release log; Drizzle migrations reviewed</td><td>0 + 1</td></tr>
<tr><td>CC9 Risk Mitigation</td><td>Mitigation of business disruption</td><td>Aurora multi-AZ + reader (Phase 1); backup retention 7-day hot + 6-year archive; DR drill quarterly</td><td>1 + 5</td></tr>
<tr><td>P (Privacy)</td><td>Personal info collection/use/disclosure</td><td>Field-level encryption (Phase 3); minimum-necessary access; PHI exclusion from reporting; BAA inventory</td><td>0, 2, 3, 5</td></tr>
<tr><td>C (Confidentiality)</td><td>Confidential info protected</td><td>KMS at rest + TLS in transit (Phase 1); field-level encryption (Phase 3); Sentry scrubbing; structured logger redaction</td><td>1 + 3</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 53 · PRE-CUTOVER GATES ================================== -->
<!-- =============================================================== -->
<section id="pre-cutover-gates">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 7 · 53</span>
<h2 style="border:0;margin:18px 0 0;">Pre-cutover hard requirements (must close before Phase 4)</h2>
</div>
<div class="callout crit">
<div class="callout-label">Non-negotiable security gates before PHI cutover</div>
<ol style="margin:6px 0 0;">
<li><strong>Sentry hardened</strong> — <code>beforeSend</code> + <code>sendDefaultPii: false</code> + lower <code>tracesSampleRate</code> in all 3 init files.</li>
<li><strong>Structured logger (Pino) with PHI redaction</strong> replacing all <code>console.log</code> calls. CI linter blocks reintroduction.</li>
<li><strong>Email columns in 9 audit tables</strong> migrated to <code>user_id</code> FK + hashed-email shadow.</li>
<li><strong>MEDICALREVIEW caseload</strong> in production with backfill — verified with medical-review team.</li>
<li><strong>MemberNote auth tightened</strong> — welon sees only assigned members' notes.</li>
<li><strong>Audit-archive S3 bucket</strong> with Object Lock Compliance (7-year), CMK-encrypted.</li>
<li><strong>CloudTrail org trail</strong> enabled with S3 destination.</li>
<li><strong>Invite tokens hashed</strong>; expiry enforced.</li>
<li><strong>preTokenGen fail-closed</strong> — any error throws.</li>
<li><strong>Cognito pool config confirmed</strong> — password policy + token TTL + advanced security documented with external pool owner.</li>
<li><strong>Field-level encryption shipped</strong> for Digital Assets credentials, SSN, bank, insurance, medical directives.</li>
<li><strong>customAuth Lambda</strong> filled in — JWT verify + tenant claim required + fail-closed.</li>
<li><strong>Deletion protection ON</strong> for all DDB tables (currently OFF).</li>
<li><strong>CI security gates green</strong> — SAST (Semgrep), SCA (Snyk/pnpm audit), secret scanning, IaC scan.</li>
<li><strong>Step-up MFA</strong> deployed on sign-document, delete-user, role-change, medical-export operations.</li>
</ol>
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 54 · EVIDENCE PACK ====================================== -->
<!-- =============================================================== -->
<section id="evidence-pack">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 7 · 54</span>
<h2 style="border:0;margin:18px 0 0;">Auditor evidence pack (deliverable at Phase 5 close)</h2>
</div>
<table>
<thead><tr><th>Evidence</th><th>Source</th><th>Format</th></tr></thead>
<tbody>
<tr><td>Tested DR plan report</td><td>Phase 5 DR Drill output</td><td>PDF in Object-Lock S3, dated</td></tr>
<tr><td>Tenant-isolation verification matrix</td><td>CI test runs across Phase 2–4</td><td>Signed test report, per-phase + final</td></tr>
<tr><td>Audit log retention proof</td><td>S3 bucket config + Object Lock policy</td><td>Inventory document + policy excerpt</td></tr>
<tr><td>Encryption inventory</td><td>Per-PHI-field × mechanism table</td><td>Inventory document; one row per field</td></tr>
<tr><td>Access-control matrix</td><td>Role × resource × action, validated by tests</td><td>Spreadsheet + linked tests</td></tr>
<tr><td>Incident response runbook</td><td>Separate doc; quarterly updates; annual rehearsal</td><td>Document + access log</td></tr>
<tr><td>Vendor BAA inventory</td><td>AWS, Sentry, Stripe, Mailchimp, UPS</td><td>BAA copies + expiration dates</td></tr>
<tr><td>Penetration test report</td><td>Annual; covers AppSync + Aurora</td><td>Signed PDF from independent firm</td></tr>
<tr><td>SAST / SCA / secret-scan reports</td><td>Per-PR + scheduled</td><td>Findings tracked to resolution</td></tr>
<tr><td>Change-control evidence</td><td>Every prod change has PR + review + release log + deploy artifact</td><td>Linkable trail per PR</td></tr>
<tr><td>Risk Assessment</td><td>This document + quarterly reviews</td><td>Versioned in repo</td></tr>
</tbody>
</table>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Part 8 of 8</div>
<h2 style="border:0;color:var(--ink);">Closeout — cost, timeline, Mike's deliverables, acceptance, open questions</h2>
<p>The final ledger. Cost (monthly + one-time), the timeline summary table, the consolidated Mike's deliverables list, acceptance criteria per phase, and the open questions that need answering before Phase 1.</p>
</div>
<!-- =============================================================== -->
<!-- ===== 55 · COST =============================================== -->
<!-- =============================================================== -->
<section id="cost">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 8 · 55</span>
<h2 style="border:0;margin:18px 0 0;">Cost</h2>
</div>
<h3>Monthly recurring (at 100k members)</h3>
<table>
<thead><tr><th>Item</th><th>USD/mo</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Aurora Serverless v2 writer</td><td>~$87 baseline; ~$700 if pinned at 8 ACU</td><td>1 ACU base, autoscale 1–8</td></tr>
<tr><td>Aurora Serverless v2 reader</td><td>~$44 baseline; ~$350 if pinned at 4 ACU</td><td>0.5 ACU base, autoscale 0.5–4</td></tr>
<tr><td>Aurora I/O (Standard tier)</td><td>$20–80</td><td>Switch to I/O-Optimized when I/O bill &gt; 30% of ACU bill</td></tr>
<tr><td>Aurora storage</td><td>$5–15</td><td>50–150 GB</td></tr>
<tr><td>Backup storage</td><td>~$10</td><td>7-day PITR free; long-tail to S3</td></tr>
<tr><td>RDS Proxy</td><td>~$22</td><td>Single Proxy serving writer + reader target groups</td></tr>
<tr><td>NAT Gateway × 2 AZ</td><td>~$70</td><td>Only dispatcher goes through NAT</td></tr>
<tr><td>VPC interface endpoints</td><td>~$30</td><td>4 endpoints × $7.50</td></tr>
<tr><td>Provisioned Concurrency</td><td>~$69 baseline; ~$140 peak</td><td>5 hot Lambdas (dispatcher + preTokenGen + customAuth + Stripe webhook + postSignUpTrigger)</td></tr>
<tr><td>S3 audit-archive + Object Lock</td><td>$2–10</td><td>Glacier after 180 days</td></tr>
<tr><td>CloudWatch logs + Insights</td><td>$30–80</td><td>Depends on log volume</td></tr>
<tr><td>WAF</td><td>~$50</td><td>1 ACL + ~10M req/mo</td></tr>
<tr style="background:var(--accent-soft);font-weight:700;"><td><strong>Total monthly baseline</strong></td><td><strong>~$540</strong></td><td>Steady state at 100k members</td></tr>
<tr style="background:var(--accent-soft);font-weight:700;"><td><strong>Total monthly peak</strong></td><td><strong>~$1,250</strong></td><td>Business hours, all ACUs scaled out, full WAF</td></tr>
</tbody>
</table>
<h3>What we save / lose</h3>
<ul>
<li><strong>DynamoDB on-demand</strong> at our scale costs ~$200–500/mo today.</li>
<li><strong>The Redshift/OpenSearch reporting pipeline we'd otherwise need</strong> costs ~$150–400/mo on its own.</li>
<li><strong>Net delta:</strong> ~$200/mo more for Aurora at baseline, but reporting + audit retention come for free.</li>
</ul>
<h3>One-time engineering cost</h3>
<table>
<thead><tr><th>Item</th><th>USD</th></tr></thead>
<tbody>
<tr><td>Internal engineering time (14–16 weeks at senior loaded rate ~$250k/yr)</td><td>~$70–80k</td></tr>
<tr><td>Aurora / RLS review (optional consultant, light)</td><td>~$0–10k</td></tr>
<tr><td>Security review (annual; baseline cost)</td><td>~$10k</td></tr>
<tr style="background:var(--accent-soft);font-weight:700;"><td><strong>One-time engineering</strong></td><td><strong>~$80–100k</strong></td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 56 · TIMELINE =========================================== -->
<!-- =============================================================== -->
<section id="timeline">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 8 · 56</span>
<h2 style="border:0;margin:18px 0 0;">Timeline summary</h2>
</div>
<table>
<thead><tr><th>Week</th><th>Phase</th><th>Deliverable</th><th>Gate</th></tr></thead>
<tbody>
<tr><td>0</td><td>Pre-flight</td><td>Decisions confirmed; BAAs verified; tooling chosen; team named</td><td>All §10 items checked</td></tr>
<tr><td>1–2</td><td>Phase 1: Foundation</td><td>VPC, Aurora, RDS Proxy, KMS, Postgres roles, Cognito attrs, Sentry hardening, preTokenGen fail-closed, WAF, CloudTrail, audit-archive</td><td>Phase 1 gate</td></tr>
<tr><td>3–6</td><td>Phase 2: Schema + non-PHI</td><td>Postgres DDL, RLS policies, Amplify connect-to-SQL, ~30 non-PHI models migrated incrementally, status-history started early, outbox</td><td>Phase 2 gate</td></tr>
<tr><td>7–10</td><td>Phase 3: Dispatcher + tenancy</td><td>customAuth, claim injection, dispatcher Lambda, field-level encryption, PC, MEDICALREVIEW caseload, MemberNote tightening, PII column refactor, step-up MFA</td><td>Phase 3 gate</td></tr>
<tr><td>11–13</td><td>Phase 4: PHI cutover</td><td>Dress rehearsal → prod cutover Friday evening → 4 h rollback window → 24 h hot monitor</td><td>Cutover successful or rolled back</td></tr>
<tr><td>14–16</td><td>Phase 5: Reporting + closeout</td><td>Reader endpoint, Reporting page, DR Drill, audit evidence pack, decommission old DDB</td><td>Phase 5 gate</td></tr>
</tbody>
</table>
<h3>Critical path</h3>
<ol>
<li>Tenant model decision (week 5) — blocks RLS-related work.</li>
<li>Field-auth projection layer (week 8) — blocks dispatcher completion.</li>
<li>Dress rehearsal (week 11) — gates prod cutover.</li>
<li>Pre-cutover hard requirements (week 12) — non-negotiable security gates.</li>
</ol>
<h3>What slips first / never slips</h3>
<table>
<thead><tr><th>Slips first</th><th>Never slips</th></tr></thead>
<tbody>
<tr><td>Week 15–16 stabilization compresses to 1 week if cutover went smoothly. DR Drill can move to week 15. Phase 3 polish explicitly post-deadline.</td><td>Pre-cutover hard requirements (PHI scrubbing, structured logger, MEDICALREVIEW caseload). Rollback rehearsal. Cutover without these creates new HIPAA exposure.</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 57 · MIKE'S DELIVERABLES ================================ -->
<!-- =============================================================== -->
<section id="mike-deliverables">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num devops">Part 8 · 57</span>
<h2 class="devops" style="border:0;margin:18px 0 0;">Mike's deliverables — one consolidated list</h2>
</div>
<h3>Phase 0 (pre-flight)</h3>
<ol>
<li>Confirm AWS BAA signed; archive PDF in audit-archive bucket.</li>
<li>Identify external Cognito pool owner; schedule attribute-add coordination.</li>
<li>Set up AWS Organizations with OUs per environment + SCPs (US regions only).</li>
<li>Set up IAM Identity Center; deactivate long-lived human IAM users.</li>
<li>Fix smart-quote bug in <code>amplify.yml</code>.</li>
<li>Measure exact CFN resource count today; track per-PR.</li>
</ol>
<h3>Phase 1 (Weeks 1–2)</h3>
<ol>
<li>Provision per-environment VPCs (3 envs × VPC each).</li>
<li>Set up 2 NAT Gateways per VPC, 6 VPC endpoints per VPC.</li>
<li>Define 3 security groups per VPC (dispatcher, RDS Proxy, Aurora).</li>
<li>Provision 4 CMKs per environment.</li>
<li>Provision Aurora Serverless v2 cluster + reader per env.</li>
<li>Enable PostgreSQL extensions (pgaudit, pg_stat_statements, pgcrypto, aws_s3).</li>
<li>Create 5 Postgres roles per env.</li>
<li>Provision RDS Proxy with IAM auth, two target groups per env.</li>
<li>Set up Performance Insights.</li>
<li>Provision S3 audit-archive bucket with Object Lock (Compliance 7-year).</li>
<li>Set up CloudTrail org trail; enable Log File Validation.</li>
<li>Provision WAF v2 Web ACL; associate with AppSync.</li>
<li>Set up the 3 CloudWatch dashboards.</li>
<li>Wire all 16 alarms.</li>
<li>Deploy Sentry hardening (all 3 init files).</li>
<li>Add 5 CI security gates to <code>amplify.yml</code>.</li>
<li>Set up Drizzle Kit migration runner in CI.</li>
<li>Coordinate Cognito custom-attribute add with pool owner.</li>
<li>Run Cognito backfill script on all 3 envs.</li>
<li>Convert preTokenGen to fail-closed.</li>
</ol>
<h3>Phase 3 (Weeks 7–10) — Mike-specific</h3>
<ol>
<li>Fill in <code>customAuth</code> Lambda.</li>
<li>Attach dispatcher + encrypt-field + decrypt-field + outbox-worker to VPC; add PC.</li>
<li>Provision Field-Encryption CMK; lock-down key policy.</li>
<li>Lazy-import Sentry across 50 Lambdas (or hand off to dev team).</li>
</ol>
<h3>Phase 4 (Weeks 11–13)</h3>
<ol>
<li>Enable DynamoDB Streams on all 41 tables (T−1 week).</li>
<li>Flip deletion protection to ON on prod tables.</li>
<li>Provision cutover-temp infrastructure.</li>
<li>Execute cutover playbook; staff on-call for 4 h window.</li>
<li>Print + tape rollback runbook beside war room.</li>
</ol>
<h3>Phase 5 (Weeks 14–16)</h3>
<ol>
<li>Provision Aurora reader endpoint via RDS Proxy reader target group.</li>
<li>Execute DR Drill; record RTO/RPO; file report in audit-archive S3.</li>
<li>Compile auditor evidence pack.</li>
<li>Decommission old DDB PHI tables after 30-day stability.</li>
<li>Schedule quarterly DR drill recurrence.</li>
</ol>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 58 · ACCEPTANCE ========================================= -->
<!-- =============================================================== -->
<section id="acceptance">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 8 · 58</span>
<h2 style="border:0;margin:18px 0 0;">Acceptance criteria per phase</h2>
</div>
<table>
<thead><tr><th>Phase</th><th>Acceptance criterion (DevOps-owned)</th></tr></thead>
<tbody>
<tr><td>0</td><td>BAA in audit-archive; pool owner identified; SCPs deployed; smart-quote bug fixed; CFN count tracked in CI</td></tr>
<tr><td>1</td><td>All Phase-1 infrastructure provisioned + verified (Aurora reachable, RDS Proxy IAM-auth working, KMS keys created, audit-archive bucket Object-Lock confirmed, WAF associated with AppSync, all 16 alarms armed, Sentry hardening live, CI security gates green on a test PR, Drizzle runner applied a no-op migration to staging)</td></tr>
<tr><td>2</td><td>Postgres DDL deployed; RLS verification matrix passing on dev-test; non-PHI models migrated to Amplify SQL one at a time with frontend tests passing unchanged; status-history writes accruing</td></tr>
<tr><td>3</td><td>customAuth Lambda gating prod; field-encryption Lambda round-trip verified; tenant-isolation matrix passing for all 15 PHI tables; MEDICALREVIEW caseload verified with team; PC providing &lt; 2 s sign-in P95</td></tr>
<tr><td>4</td><td>Cutover dress rehearsal completed twice in dev-test under 1 h rollback; prod cutover successful with RLS denial rate = 0, error rate within baseline, P95 latency within 20% of pre-cutover</td></tr>
<tr><td>5</td><td>DR Drill report shows RTO &lt; 4 h / RPO &lt; 5 min; auditor evidence pack assembled; old DDB PHI tables deleted; two consecutive weeks of stable production</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== 59 · OPEN QUESTIONS ===================================== -->
<!-- =============================================================== -->
<section id="openq">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Part 8 · 59</span>
<h2 style="border:0;margin:18px 0 0;">Open questions for the team</h2>
</div>
<table>
<thead><tr><th>#</th><th>Question</th><th>Who answers</th></tr></thead>
<tbody>
<tr><td>1</td><td>Are dev-test / staging / prod in separate AWS accounts today, or one account with multiple stacks?</td><td>Mike</td></tr>
<tr><td>2</td><td>Who owns the external Cognito pool IaC? What's the lead time for custom-attribute add across 3 envs?</td><td>Mike + pool owner</td></tr>
<tr><td>3</td><td>What is the current Cognito password policy + access/id/refresh token TTLs?</td><td>Cognito pool owner</td></tr>
<tr><td>4</td><td>Advanced security features (Plus tier — compromised credentials, adaptive auth) enabled?</td><td>Cognito pool owner</td></tr>
<tr><td>5</td><td>Exact current CFN resource count today, and after Phase 0 cleanup?</td><td>Mike (precise count)</td></tr>
<tr><td>6</td><td>How many DDB items in <code>Document</code> table currently exceed 300 KB? (per D2)</td><td>Mike (scan command in D2)</td></tr>
<tr><td>7</td><td>Confirm BAA tier on Sentry — Enterprise? Business with custom BAA?</td><td>Mike + legal</td></tr>
<tr><td>8</td><td>Cognito attribute coordination may take 2+ weeks if pool owner is external — acceptable?</td><td>Jon + Mike</td></tr>
<tr><td>9</td><td>Do we have AWS Identity Center, or are humans using IAM users with access keys?</td><td>Mike</td></tr>
<tr><td>10</td><td>Secret rotation policy today for STRIPE_SECRET_KEY / MAILCHIMP_API_KEY / UPS_CLIENT_SECRET / GOOGLE_API_KEY / FAX_CLIENT_SECRET? Any in violation?</td><td>Mike</td></tr>
<tr><td>11</td><td>US-only geo-block on WAF — hard rule, or legitimate non-US access (vacation)?</td><td>Jon (product)</td></tr>
<tr><td>12</td><td>IAM Identity Center + temp credentials for chil_admin DB break-glass, or long-lived password fallback?</td><td>Mike</td></tr>
<tr><td>13</td><td>DR cross-region (us-east-1 → us-west-2) required, or single-region snapshot-restore sufficient?</td><td>Mike + auditor preference</td></tr>
<tr><td>14</td><td>Self-serve sign-up tenant resolution — domain? URL? Default to seed?</td><td>Jon (product)</td></tr>
<tr><td>15</td><td>Templates — shared base + per-tenant overrides, or fully per-tenant?</td><td>Jon + Oleksii</td></tr>
<tr><td>16</td><td>MEDICALREVIEW access — scoped (caseload model) or all-members?</td><td>Jon + medical-team lead</td></tr>
<tr><td>17</td><td>WELONTRUST CRUD on every MemberNote — intended? If "no," HIPAA gap to close.</td><td>Jon</td></tr>
<tr><td>18</td><td>Will Childfree internal staff need cross-tenant access (platform support)? If yes, design as audited "platform" scope.</td><td>Jon + Mike</td></tr>
<tr><td>19</td><td>Platform ever SaaS to multiple trust companies? (Influences multi-tenancy lifecycle.)</td><td>Jon (product)</td></tr>
<tr><td>20</td><td>14–16 week timeline acceptable, or descope?</td><td>Jon + Mike + leadership</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== GLOSSARY ================================================ -->
<!-- =============================================================== -->
<section id="glossary">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · A</span>
<h2 style="border:0;margin:18px 0 0;">Glossary</h2>
</div>
<table>
<tbody>
<tr><td><strong>ACU</strong></td><td>Aurora Capacity Unit. Aurora Serverless v2 scales by ACU; ~1 ACU ≈ 2 GB RAM + matched CPU.</td></tr>
<tr><td><strong>BAA</strong></td><td>Business Associate Agreement. HIPAA contract required with every vendor processing PHI.</td></tr>
<tr><td><strong>BYPASSRLS</strong></td><td>Postgres role attribute letting the role ignore RLS policies.</td></tr>
<tr><td><strong>CDC</strong></td><td>Change Data Capture. Streaming row changes from one store to another.</td></tr>
<tr><td><strong>CFN</strong></td><td>CloudFormation. AWS IaC; 500 resources per stack — the limit we hit.</td></tr>
<tr><td><strong>CMK</strong></td><td>Customer-Managed Key (KMS). Required for HIPAA workloads.</td></tr>
<tr><td><strong>ENI</strong></td><td>Elastic Network Interface. VPC-attached Lambdas allocate ENIs; soft limit 250/region.</td></tr>
<tr><td><strong>GUC</strong></td><td>Grand Unified Configuration — Postgres session variable. <code>app.trust_company_id</code> etc. drive RLS.</td></tr>
<tr><td><strong>JWT</strong></td><td>JSON Web Token. Cognito access token; PreTokenGen injects custom claims.</td></tr>
<tr><td><strong>KMS</strong></td><td>AWS Key Management Service.</td></tr>
<tr><td><strong>Object Lock</strong></td><td>S3 feature; objects cannot be deleted/modified until retention elapses.</td></tr>
<tr><td><strong>PC</strong></td><td>Provisioned Concurrency (Lambda). Pre-warmed instances that bypass cold start.</td></tr>
<tr><td><strong>pgaudit</strong></td><td>Postgres extension logging SELECT/INSERT/UPDATE/DELETE on tagged tables.</td></tr>
<tr><td><strong>PHI</strong></td><td>Protected Health Information (HIPAA).</td></tr>
<tr><td><strong>PII</strong></td><td>Personally Identifiable Information.</td></tr>
<tr><td><strong>PITR</strong></td><td>Point-In-Time Recovery. Continuous backup.</td></tr>
<tr><td><strong>RLS</strong></td><td>Row-Level Security (Postgres). Policies that filter rows by session context.</td></tr>
<tr><td><strong>RPO / RTO</strong></td><td>Recovery Point / Time Objective.</td></tr>
<tr><td><strong>SOC 2</strong></td><td>Service Organization Control 2. Trust principles audit.</td></tr>
<tr><td><strong>VPC Endpoint</strong></td><td>Private connection from VPC to AWS service without NAT/internet.</td></tr>
<tr><td><strong>WAF</strong></td><td>Web Application Firewall. AWS WAF v2 in front of AppSync.</td></tr>
</tbody>
</table>
</section>
<hr>
<div class="part-banner">
<div class="part-num">Appendix B</div>
<h2 style="border:0;color:var(--ink);">Architecture review — industry-standard frameworks applied to this document</h2>
<p>This appendix re-reviews the entire migration plan through seven canonical architecture review lenses — <strong>arc42</strong> (documentation completeness), <strong>C4</strong> (visual decomposition), <strong>ATAM</strong> (SEI's Architecture Tradeoff Analysis Method: utility tree, risk themes, sensitivity &amp; tradeoff points), <strong>AWS Well-Architected Framework</strong> (six-pillar scoring), <strong>Evolutionary Architecture</strong> (Ford/Parsons fitness functions), <strong>MADR/Nygard ADRs</strong> (decision-record inventory), and <strong>SOC 2 / HIPAA mapping</strong> (cross-referenced against Parts 7.51–54). The goal is to surface gaps the prior 8 parts may have buried in narrative — and to give Mike, Jon, and Oleksii a self-contained quality bar to hold each Phase against.</p>
</div>
<!-- =============================================================== -->
<!-- ===== B.1 · METHODOLOGY ======================================= -->
<!-- =============================================================== -->
<section id="review-method">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.1</span>
<h2 style="border:0;margin:18px 0 0;">Review methodology — the seven lenses</h2>
</div>
<table>
<thead><tr><th>Lens</th><th>What it asks</th><th>What it would catch</th><th>Why it matters here</th></tr></thead>
<tbody>
<tr><td><strong>arc42</strong> <span style="color:var(--muted);font-weight:400;">(Starke / Hruschka, canonical 12-section template)</span></td><td>Is every standard documentation section present? (Context, Constraints, Solution Strategy, Building Block, Runtime, Deployment, Crosscutting, Decisions, Quality, Risks, Glossary)</td><td>Missing diagrams or untold context that would surface only after onboarding pain</td><td>Onboarding next engineer; auditor evidence completeness</td></tr>
<tr><td><strong>C4 Model</strong> <span style="color:var(--muted);font-weight:400;">(Simon Brown — Context / Container / Component / Code)</span></td><td>Are System Context, Container, and Component levels each visualized?</td><td>Implicit topology that nobody but the author understands</td><td>Mike + Oleksii need to share a mental model of "what calls what"</td></tr>
<tr><td><strong>ATAM utility tree</strong> <span style="color:var(--muted);font-weight:400;">(SEI Architecture Tradeoff Analysis Method — Kazman / Klein / Clements)</span></td><td>Each quality attribute → refinement → concrete measurable scenario</td><td>Vague NFRs ("system should be fast") with no acceptance threshold</td><td>Phase gates need quantitative pass/fail thresholds, not "looks ok"</td></tr>
<tr><td><strong>ATAM risk themes</strong></td><td>What systemic patterns appear in 3+ risks?</td><td>Single-discipline mitigations missing the underlying common cause</td><td>Tells leadership what's actually fragile vs what's just a checklist item</td></tr>
<tr><td><strong>Sensitivity &amp; tradeoff points</strong></td><td>Which parameter changes cascade? Where are quality attributes in conflict?</td><td>Hidden assumptions that break the whole plan when revised</td><td>Reveals what NOT to change without re-review</td></tr>
<tr><td><strong>AWS Well-Architected</strong></td><td>Score per pillar (Operational Excellence, Security, Reliability, Performance, Cost, Sustainability) against AWS 2024 framework + April 2025 best-practice additions</td><td>Anything AWS already says is best practice for HIPAA workloads but we missed</td><td>Auditor will run this; better to self-score first</td></tr>
<tr><td><strong>Fitness functions</strong> <span style="color:var(--muted);font-weight:400;">(Ford / Parsons / Kua — <em>Building Evolutionary Architectures</em>, 2nd ed.)</span></td><td>What's enforced automatically in CI vs left as "team discipline"?</td><td>Drift between intent and reality once the plan ships</td><td>HIPAA controls degrade silently without automated enforcement</td></tr>
<tr><td><strong>MADR / Nygard ADRs</strong> <span style="color:var(--muted);font-weight:400;">(Markdown Architecture Decision Records — Nygard 2011 + MADR 4.0 template)</span></td><td>Is every architecturally significant decision recorded with Context / Decision / Consequences?</td><td>Future "why did we…" arguments where nobody remembers the constraint</td><td>Two-year horizon: people leave; ADRs survive turnover</td></tr>
<tr><td><strong>HIPAA / SOC 2 mapping</strong></td><td>Every Safeguard / Trust Service Criterion → which phase delivers it</td><td>Compliance gaps where intent is unclear which phase owns it</td><td>Auditor evidence pack (§54) is built from this mapping</td></tr>
</tbody>
</table>
<div class="callout">
<div class="callout-label">How to read the verdicts in this appendix</div>
<strong class="ok">PASS</strong> — addressed at expected depth.<br>
<strong style="color:var(--warn);">PARTIAL</strong> — addressed but with a gap below.<br>
<strong class="crit">GAP</strong> — not addressed; closure item issued.<br>
Each gap maps to a specific <strong>§B.11 gap-closure</strong> deliverable so they're not lost.
</div>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.2 · ARC42 AUDIT ======================================= -->
<!-- =============================================================== -->
<section id="arc42-audit">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.2</span>
<h2 style="border:0;margin:18px 0 0;">arc42 coverage audit — twelve sections, twelve verdicts</h2>
</div>
<p>arc42 is the canonical 12-section documentation template for software architecture. A document that hits all twelve is self-contained; one that misses several leaves a maintainer guessing. Below: each arc42 section, mapped to where this document addresses it, with verdict.</p>
<table>
<thead><tr><th>#</th><th>arc42 section</th><th>Where in this doc</th><th>Verdict</th><th>Gap / note</th></tr></thead>
<tbody>
<tr><td>1</td><td>Introduction &amp; Goals</td><td>Part 1 §00 Executive summary; §01 Why Aurora; companion <code>CALL-PREP-UNIFIED.html</code> for full context</td><td><span class="sev verified">PASS</span></td><td>—</td></tr>
<tr><td>2</td><td>Constraints</td><td>Sprinkled: §3a-cognito (external Cognito pool), §6 CFN limits, §29 PC budget, §28 ENI cliff, §53 pre-cutover hard requirements, §59 open questions</td><td><span class="sev warn">PARTIAL</span></td><td>No single "Constraints" section consolidates them. Closure: <strong>§B.11 #1 Constraints Register</strong>.</td></tr>
<tr><td>3</td><td>System Scope &amp; Context</td><td>Implicit — external systems named scattered (Cognito external pool, Stripe, Mailchimp, UPS, Botpress, Sentry, Fax client, Vimeo)</td><td><span class="sev high">GAP</span></td><td>No C4 Level 1 System Context diagram. Closure: <strong>§B.3 below</strong>.</td></tr>
<tr><td>4</td><td>Solution Strategy</td><td>Part 1 §03 Target topology + Part 3 §06 dispatcher pattern</td><td><span class="sev verified">PASS</span></td><td>—</td></tr>
<tr><td>5</td><td>Building Block View</td><td>Part 3 §06–09 narrative; DevOps items 19, 21, 27, 28</td><td><span class="sev warn">PARTIAL</span></td><td>Excellent narrative; no C4 Container diagram. Closure: <strong>§B.3 below</strong>.</td></tr>
<tr><td>6</td><td>Runtime View</td><td>Part 4 phases; cutover playbook; <code>SET LOCAL</code> code blocks</td><td><span class="sev warn">PARTIAL</span></td><td>No sequence diagrams for: (a) PHI read with RLS, (b) cutover flip, (c) rollback. Closure: <strong>§B.11 #2 Sequence diagrams</strong>.</td></tr>
<tr><td>7</td><td>Deployment View</td><td>Part 4 + DevOps items 17–48</td><td><span class="sev verified">PASS</span></td><td>—</td></tr>
<tr><td>8</td><td>Crosscutting Concepts</td><td>RLS (§07), multi-tenancy (§07), encryption (§20 + §43), audit logging (§33 + §34), observability (§41 + §42)</td><td><span class="sev verified">PASS</span></td><td>—</td></tr>
<tr><td>9</td><td>Architecture Decisions</td><td>Embedded in narrative; not extracted as ADRs</td><td><span class="sev high">GAP</span></td><td>No ADR repository. Closure: <strong>§B.9 below</strong>.</td></tr>
<tr><td>10</td><td>Quality Requirements</td><td>HIPAA/SOC 2 in §51–52; performance &amp; reliability in scattered places</td><td><span class="sev warn">PARTIAL</span></td><td>No utility tree with scenarios. Closure: <strong>§B.4 below</strong>.</td></tr>
<tr><td>11</td><td>Risks &amp; Technical Debt</td><td>Part 2 §05 D1–D6; §53 pre-cutover gates; §59 open questions</td><td><span class="sev verified">PASS</span></td><td>Risk themes pass below adds aggregation layer.</td></tr>
<tr><td>12</td><td>Glossary</td><td>Appendix A</td><td><span class="sev verified">PASS</span></td><td>—</td></tr>
</tbody>
</table>
<p><strong>arc42 score: 7 PASS / 4 PARTIAL / 2 GAP.</strong> Two systemic gaps (no C4 diagrams; no ADRs) drive most of the partials too. Closing those two collapses the deficit.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.3 · C4 MODEL ========================================== -->
<!-- =============================================================== -->
<section id="c4-model">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.3</span>
<h2 style="border:0;margin:18px 0 0;">C4 Model — Context, Container, Component diagrams</h2>
</div>
<p>The C4 model (Simon Brown) layers four views: System Context (people + external systems), Container (deployable units), Component (inside one container), and Code (class-level — rarely needed). Three views are reproduced below to close the arc42 §3 / §5 / §6 gaps.</p>
<h3>B.3.1 — System Context (C4 Level 1)</h3>
<pre class="sh"><code>┌────────────────────────────────────────────────────────────────────┐
│ People │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ MEMBER │ │ WELON │ │ MEDICAL │ │ ADMIN │ │
│ │ (PHI owner) │ │ TRUST │ │ REVIEW │ │ staff │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬────┘ │
│ │ │ │ │ │
│ └────────────────┴────────────────┴───────────────┘ │
│ ↓ HTTPS │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Childfree Legacy platform │ │
│ │ (this system) │ │
│ │ │ │
│ └─────┬──────────────────────┬─────────────────────┬───────────┘ │
│ │ OAuth/JWT │ Webhooks │ S3 / E-mail │
│ ↓ ↓ ↓ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
│ │ Cognito │ │ Stripe │ │ SES / Mailchimp │ │
│ │ User Pool │ │ (payments) │ │ (transactional + │ │
│ │ (external) │ │ │ │ marketing email) │ │
│ └──────────────┘ └──────────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
│ │ Sentry │ │ UPS Shipping API │ │ Botpress chatbot │ │
│ │ (errors, │ │ (death cert │ │ (member help) │ │
│ │ Enterprise) │ │ fulfillment) │ │ │ │
│ └──────────────┘ └──────────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ FAX provider │ │ Vimeo │ │
│ │ (hospital │ │ (training video) │ │
│ │ delivery) │ │ │ │
│ └──────────────┘ └──────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>People = 4 personas.</strong> <strong>External systems = 8.</strong> All 8 BAA status confirmed in §10 Pre-flight checklist.</p>
<h3>B.3.2 — Container (C4 Level 2)</h3>
<p>Internal containers of Childfree Legacy that this document deploys:</p>
<pre class="sh"><code>┌──────────────────────────────────────────────────────────────────────────────────┐
│ Childfree Legacy — internal │
│ │
│ ┌──────────────────────┐ │
│ │ Next.js frontend │ ← deployed to Amplify Hosting │
│ │ (App Router) │ CSP nonce + HSTS + WAF │
│ └─────────┬────────────┘ │
│ │ AppSync GraphQL (TLS) │
│ ↓ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ customAuth Lambda │←─│ AppSync API │ ← Lambda auth + PC │
│ │ (JWT verify, │ │ (37 entities) │ WAF v2 attached │
│ │ tenant injection) │ └─────────┬────────────┘ │
│ └──────────────────────┘ │ │
│ │ │
│ ┌─────────────────────────────┴─────────────┐ │
│ │ non-PHI PHI ↓ │
│ ↓ │
│ ┌──────────────────┐ ┌──────────────────────────────────────┐ │
│ │ Amplify │ │ dispatcher Lambda │ ← inside VPC │
│ │ connect-to-SQL │ │ (pg, parameterized, SET LOCAL, │ PC 1, env CMK │
│ │ (passthrough, │ │ audit emit) │ │
│ │ no Lambda) │ └─────────┬────────────────────────────┘ │
│ └────────┬─────────┘ │ │
│ ↓ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ RDS Proxy (IAM auth) │ ← inside VPC, env CMK │
│ └──────────┬──────────────────────────────────┘ │
│ │ │
│ writer │ ┌─────────────┐ │
│ ↓ ↓ reader │ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Aurora Serverless v2 (Postgres 16) │ ← Multi-AZ, │
│ │ chil_app (RLS) · chil_sqlconnect (RLS) · │ PITR 30d, │
│ │ chil_reporter (RLS RO) · chil_admin (BYPASS) · │ pgaudit, │
│ │ chil_migrator (DDL) │ env CMK │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ DDB (post-cutover): │ │ S3 PHI buckets │ │ S3 audit-archive │ │
│ │ 5 high-write tables │ │ (6 path prefixes) │ │ Object Lock 7-yr │ │
│ │ Streams enabled │ │ env CMK │ │ Compliance mode │ │
│ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ encrypt-field Lambda │ │ decrypt-field Lambda │ ← inside VPC, field CMK │
│ │ (Encryption SDK) │ │ (Encryption SDK) │ PC 1 each │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ preTokenGen Lambda │ │ Stripe webhook │ │ outbox-worker Lambda │ │
│ │ PC 1 │ │ Lambda PC 1 │ │ inside VPC, PC 1 │ │
│ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ ~70 other Lambdas (non-VPC, no PC) — no DB access, S3/SES/API only │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Containers in VPC ≤ 5</strong> (dispatcher, encrypt-field, decrypt-field, outbox-worker, customAuth if VPC-attached) <strong>+ RDS Proxy + Aurora writer + Aurora reader</strong>. Remaining ~70 Lambdas (out of 74 total) stay outside the VPC. ENI cliff respected by design (§28).</p>
<h3>B.3.3 — Component (C4 Level 3) — inside the dispatcher Lambda</h3>
<pre class="sh"><code>┌────────────────────────────────────────────────────────────────────────────┐
│ dispatcher Lambda (Node.js 22, Postgres pg driver) │
│ │
│ ┌──────────────────────────┐ │
│ │ AppSync resolver event │ ← typeName + fieldName + JWT context │
│ └─────────────┬────────────┘ │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ tenant-extractor │ ← read trust_company_id from JWT │
│ └─────────────┬────────────┘ fail-closed if missing │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ operation router │ ← typeName.fieldName → SQL builder │
│ └─────────────┬────────────┘ deterministic mapping │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ SQL builder │ ← parameterized only; no string concat │
│ └─────────────┬────────────┘ │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ pg pool │ ← single connection from pool │
│ └─────────────┬────────────┘ BEGIN; SET LOCAL app.trust_company_id │
│ │ SET LOCAL app.role; SQL; COMMIT/ROLLBACK │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ result projection layer │ ← field-auth: filter fields by JWT role │
│ └─────────────┬────────────┘ (the "field-auth projection" §13) │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ audit emitter │ ← write audit_event row in same tx │
│ └─────────────┬────────────┘ (transactional outbox pattern) │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ structured logger (Pino) │ ← PHI scrubbed; correlation_id; tenant_id │
│ └─────────────┬────────────┘ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ Sentry exception capture │ ← beforeSend PHI-scrubber │
│ └──────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>7 components</strong> inside dispatcher. Field-auth projection (4th from top) is the critical correctness layer; it's where the existing 26 <code>ownerDefinedIn('cognitoId').identityClaim('externalId')</code> rules get re-implemented in code.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.4 · UTILITY TREE ====================================== -->
<!-- =============================================================== -->
<section id="utility-tree">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.4</span>
<h2 style="border:0;margin:18px 0 0;">ATAM utility tree — quality attribute scenarios with thresholds</h2>
</div>
<p>SEI's ATAM utility tree decomposes each quality attribute into refinements, then to concrete scenarios with quantitative thresholds. Each scenario is a six-part contract: <em>source · stimulus · environment · artifact · response · response-measure</em>. Phase gates must pass these as numerical assertions, not gut feel.</p>
<table>
<thead><tr><th>Quality attribute</th><th>Refinement</th><th>Scenario (stimulus → response · threshold)</th><th>Verifier</th><th>Phase</th></tr></thead>
<tbody>
<tr><td rowspan="4"><strong>Security</strong></td>
<td>Multi-tenant isolation</td>
<td>User with tenant A's JWT issues <code>list Document</code> → response contains <strong>0</strong> rows belonging to tenant B</td>
<td>Integration test in CI (the "Tenant Isolation Matrix")</td>
<td>2 (initial), 3 (PHI tables)</td></tr>
<tr><td>Encryption at rest</td>
<td>Aurora storage scanned with AWS Config rule → <strong>100%</strong> encrypted with environment CMK (not aws/rds)</td>
<td>AWS Config managed rule <code>rds-storage-encrypted</code></td>
<td>1</td></tr>
<tr><td>Audit non-repudiation</td>
<td>Delete row on PHI table → <strong>1</strong> matching <code>audit_event</code> row visible to <code>chil_audit</code> within 5 s</td>
<td>Synthetic probe Lambda every 5 min</td>
<td>2</td></tr>
<tr><td>Fail-closed auth</td>
<td>customAuth Lambda throws → AppSync returns <strong>401</strong>, <strong>0</strong> rows leaked</td>
<td>Chaos test in dress rehearsal</td>
<td>3 + 4</td></tr>
<tr><td rowspan="4"><strong>Performance Efficiency</strong></td>
<td>Read latency (steady state)</td>
<td>100k members, business hours, member loads dashboard → P95 dispatcher duration <strong>&lt; 400 ms</strong>, P99 <strong>&lt; 800 ms</strong></td>
<td>CloudWatch Lambda <code>Duration</code> P95/P99 alarms</td>
<td>3 + 5</td></tr>
<tr><td>Cold start (sign-in)</td>
<td>First request after 60 min idle → preTokenGen total <strong>&lt; 2000 ms</strong> (the Cognito 5 s timeout has 2.5× headroom)</td>
<td>Synthetic canary every 60 min</td>
<td>1 (Sentry hardening) + 3 (PC)</td></tr>
<tr><td>Write throughput</td>
<td>Stripe webhook burst of 100 events/s → <strong>0</strong> dispatcher errors, P95 &lt; 800 ms</td>
<td>Load test before cutover</td>
<td>4 (dress rehearsal)</td></tr>
<tr><td>Aurora capacity headroom</td>
<td>Steady-state ACU usage at peak hour → <strong>&lt; 70%</strong> of max ACU; <strong>&gt; 30%</strong> headroom</td>
<td>CloudWatch <code>ServerlessDatabaseCapacity</code> alarm</td>
<td>5</td></tr>
<tr><td rowspan="3"><strong>Reliability</strong></td>
<td>Recovery point</td>
<td>Catastrophic failure at T → recover all PHI tables to T−5 min</td>
<td>DR Drill report (§45)</td>
<td>5 + quarterly</td></tr>
<tr><td>Recovery time</td>
<td>Catastrophic failure declared → service restored within <strong>4 h</strong></td>
<td>DR Drill report</td>
<td>5 + quarterly</td></tr>
<tr><td>Cutover reversibility</td>
<td>Cutover at T → rollback possible at T+4h without data loss</td>
<td>Dress rehearsal × 2 in dev-test</td>
<td>4</td></tr>
<tr><td rowspan="3"><strong>Operability</strong></td>
<td>Schema change deploy</td>
<td>Single-column add → CI deploy &lt; <strong>30 min</strong> end-to-end (no manual steps)</td>
<td>Drizzle Kit runner in CI</td>
<td>1 + 2</td></tr>
<tr><td>On-call incident response</td>
<td>Alarm fires → on-call sees relevant context in dashboard within <strong>2 min</strong></td>
<td>CloudWatch dashboard (§41) + drill</td>
<td>1 + 5</td></tr>
<tr><td>Rollback drill</td>
<td>Rollback runbook step-by-step exact CLI → completes &lt; <strong>30 min</strong> on rehearsal</td>
<td>Dress rehearsal × 2</td>
<td>4</td></tr>
<tr><td rowspan="2"><strong>Modifiability</strong></td>
<td>New tenant onboarding</td>
<td>New trust company → live in &lt; <strong>2 h</strong> (just an INSERT + Cognito group)</td>
<td>Smoke test on dev-test</td>
<td>2</td></tr>
<tr><td>New access pattern</td>
<td>"Get members by status sorted by date" → <strong>1</strong> CREATE INDEX, <strong>0</strong> backfill steps</td>
<td>Code review verifies no GSI; just an index</td>
<td>2 onward</td></tr>
<tr><td rowspan="2"><strong>Cost</strong></td>
<td>Monthly steady-state</td>
<td>100k members, business-hours load profile → <strong>&lt; $1,500/mo</strong> total AWS bill (Aurora + Proxy + Lambda + S3 + CloudWatch)</td>
<td>AWS Cost Anomaly + monthly review</td>
<td>5</td></tr>
<tr><td>One-time engineering</td>
<td>Project complete → <strong>≤ $100k</strong> engineering effort (~14–16 weeks)</td>
<td>Time tracking / phase-gate review</td>
<td>5</td></tr>
<tr><td><strong>Compliance</strong></td>
<td>HIPAA + SOC 2 audit-ready</td>
<td>Auditor request "show me audit trail for member X over Y" → returnable from <strong>S3 audit-archive query in &lt; 1 day</strong>; coverage <strong>100%</strong> of mutating ops</td>
<td>Quarterly evidence-pack rehearsal</td>
<td>5 + recurring</td></tr>
</tbody>
</table>
<p><strong>18 scenarios with quantitative thresholds.</strong> Each phase gate (§58 Acceptance criteria) is restated below as "scenarios closed at this phase":</p>
<table>
<thead><tr><th>Phase</th><th>Scenarios closed</th></tr></thead>
<tbody>
<tr><td>1</td><td>Encryption at rest; Cold start (without PC, baseline); Schema change deploy; On-call incident response (dashboards live)</td></tr>
<tr><td>2</td><td>Multi-tenant isolation (non-PHI); Audit non-repudiation; New tenant onboarding; New access pattern</td></tr>
<tr><td>3</td><td>Multi-tenant isolation (PHI); Fail-closed auth; Cold start (with PC)</td></tr>
<tr><td>4</td><td>Cutover reversibility; Write throughput; Rollback drill</td></tr>
<tr><td>5</td><td>Recovery point + time; Read latency steady-state; Aurora capacity headroom; Monthly cost; HIPAA + SOC 2 audit-ready</td></tr>
</tbody>
</table>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.5 · RISK THEMES ======================================= -->
<!-- =============================================================== -->
<section id="risk-themes">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.5</span>
<h2 style="border:0;margin:18px 0 0;">ATAM risk themes — patterns that appear 3+ times</h2>
</div>
<p>Risk themes are systemic patterns. Individual risks are easier to mitigate; themes are easier to miss. Each theme below points to multiple discovered risks across the prior 8 parts.</p>
<div class="review-block">
<h4>RT-1 · Coordination dependency on external Cognito pool owner <span class="sev crit">CRITICAL</span></h4>
<p><strong>Where it appears:</strong> §10 Pre-flight (lead-time risk); §26 (custom-attribute add coordination); §53 (Cognito pool config confirmed); §59 questions #2 / #4 / #8.</p>
<p><strong>Underlying pattern:</strong> The Cognito pool is owned externally. Any attribute change, password-policy change, advanced-security-feature toggle, or trigger redeploy requires a separate team's calendar. This single dependency gates Phase 1 + Phase 3.</p>
<p><strong>Cross-theme mitigation:</strong> Phase 0 must extract a written SLA from the pool owner: max 5 business days to apply attribute changes; their on-call contact published. Failure to secure this → reschedule cutover.</p>
</div>
<div class="review-block">
<h4>RT-2 · Implicit fail-open behavior throughout legacy code <span class="sev crit">CRITICAL</span></h4>
<p><strong>Where it appears:</strong> §05 D4 (preTokenGen catch returns event); §11 (Phase 1 fix); §53 #9 (hard gate before cutover); §B.8 FF2 (automated enforcement); customAuth current stub <code>isAuthorized = true</code>; the <code>catch → return event</code> pattern recurs across 5+ Lambdas.</p>
<p><strong>Underlying pattern:</strong> The codebase defaults to "log and proceed" on auth/data errors. Under HIPAA, this is the exact opposite of correct — auth errors must <em>fail closed</em>, returning 401 with no data. The pattern repeats across multiple Lambdas because Cognito's "Trigger failure aborts sign-in" UX is harsh, so devs coded around it.</p>
<p><strong>Cross-theme mitigation:</strong> Code linter rule: <code>catch</code> blocks in <code>amplify/functions/</code> must <code>throw</code> or return error response. Block <code>return event</code> in catch. Add to <code>§B.8 fitness functions</code>.</p>
</div>
<div class="review-block">
<h4>RT-3 · CloudFormation scale ceiling <span class="sev high">HIGH</span></h4>
<p><strong>Where it appears:</strong> §04 (74 Lambdas in <code>backend.ts</code>); §06 (CFN 500-resource limit hit before cleanup); §17 (account boundary); жалоби.txt item 2 (dependency hell); §59 question #5 (exact CFN resource count today).</p>
<p><strong>Underlying pattern:</strong> Amplify Gen 2 deploys via CloudFormation; each model auto-generates ~5–8 CFN resources. At 41 models × ~5 resources + 74 Lambdas × ~3 resources we are bumping against 500-per-stack and may already be close. Adding Aurora resources without proper consolidation pushes us over.</p>
<p><strong>Cross-theme mitigation:</strong> CI fitness function: warn at 400 resources/stack, fail at 480. Also: Track B's <code>a.combine()</code> + connect-to-SQL pattern naturally consolidates Postgres-backed models into one stack; the 5 remaining DDB tables stay in original stack. Track migration progress as resource-count delta per phase.</p>
</div>
<div class="review-block">
<h4>RT-4 · Cold-start tax paid 50× <span class="sev high">HIGH</span></h4>
<p><strong>Where it appears:</strong> §05 D3 (top-level await across 50 Lambdas); §30 (cold-start mitigation beyond PC); §29 (PC budget covers only 5); жалоби.txt item 5 (cold starts).</p>
<p><strong>Underlying pattern:</strong> Many Lambdas import Sentry, AWS SDK, etc. at module load, paying 100–500 ms cold-start tax on every cold invocation. PC fixes only the 5 hottest. The rest live with the tax.</p>
<p><strong>Cross-theme mitigation:</strong> Lazy-import refactor pattern: <code>const sentry = await import('@sentry/...');</code> inside handler. Apply incrementally; CI fitness function flags top-level imports of heavy SDKs.</p>
</div>
<div class="review-block">
<h4>RT-5 · Encryption-as-checklist instead of as-default <span class="sev high">HIGH</span></h4>
<p><strong>Where it appears:</strong> §20 (one CMK per concern); §43 (Sentry scrubbing scope); §51 §164.312(a)(2)(iv); §53 #11 (field-level encryption shipped); §51 §164.312(b) audit controls.</p>
<p><strong>Underlying pattern:</strong> The plan explicitly lists which fields get field-level encryption (Phase 3) and which don't. But the structured-logger PII scrubber + Sentry scrubber + Pino redaction must independently maintain matching key lists. A new PHI field added by a future PR can leak through one of these layers if the lists drift.</p>
<p><strong>Cross-theme mitigation:</strong> Single source of truth: <code>amplify/data/phi-fields.json</code> generated from schema annotations. All scrubbers import from it. CI fitness function: every <code>PHI: true</code> field has a Pino redaction entry + Sentry scrubber entry + field-encryption coverage decision.</p>
</div>
<div class="review-block">
<h4>RT-6 · Single-path PHI dispatcher <span class="sev high">HIGH</span></h4>
<p><strong>Where it appears:</strong> §06 dispatcher; §27 customAuth; §29 PC for dispatcher; §42 dispatcher error-rate alarm; §47 rollback mechanics.</p>
<p><strong>Underlying pattern:</strong> Every PHI read/write traverses the dispatcher Lambda. A single bug or misconfig there can hide an entire tenant's data or expose another's. Centralization is the right design (much smaller attack surface than per-resolver auth), but it concentrates blast radius.</p>
<p><strong>Cross-theme mitigation:</strong> The Tenant Isolation Matrix in CI is the single most important fitness function in the system. Must run on every dispatcher change. Mandatory PR reviewer for dispatcher = Oleksii (architecture review of the SQL change) + Mike (deployment review of the IaC change).</p>
</div>
<div class="review-block">
<h4>RT-7 · Drift between intent and reality after Phase 5 closes <span class="sev medium">MEDIUM</span></h4>
<p><strong>Where it appears:</strong> §10 quarterly review; §45 DR Drill quarterly; §52 CC4 monitoring; §54 evidence pack annually; §58 ongoing.</p>
<p><strong>Underlying pattern:</strong> Every prior migration of this scope eventually drifts: alarms get muted, drills slip, ADRs aren't updated, CMK rotation lapses. Phase 5 hands over to operations; without enforcement, the architecture goes stale.</p>
<p><strong>Cross-theme mitigation:</strong> Every fitness function in §B.8 runs in CI <em>and</em> on a schedule (CloudWatch Events). Quarterly architecture review with this appendix as the rubric; failures generate Jira tickets automatically.</p>
</div>
<p><strong>Risk theme count: 7 (2 CRITICAL · 4 HIGH · 1 MEDIUM).</strong></p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.6 · SENSITIVITY / TRADEOFFS ============================ -->
<!-- =============================================================== -->
<section id="sensitivity">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.6</span>
<h2 style="border:0;margin:18px 0 0;">ATAM sensitivity points &amp; tradeoff points</h2>
</div>
<p><strong>Sensitivity point</strong> = a parameter where a small change cascades into a quality attribute response. <strong>Tradeoff point</strong> = a parameter where improving one attribute degrades another.</p>
<h3>B.6.1 — Sensitivity points</h3>
<table>
<thead><tr><th>#</th><th>Parameter</th><th>Sensitive to</th><th>If changed, what re-review</th></tr></thead>
<tbody>
<tr><td>S1</td><td>JWT claim shape (<code>trust_company_id</code> + <code>role</code>)</td><td>All RLS policies; customAuth contract; dispatcher tenant-extractor</td><td>Every RLS policy file + customAuth + dispatcher unit tests</td></tr>
<tr><td>S2</td><td>Postgres connection pool size in dispatcher</td><td>Aurora connection saturation + cold-start cost</td><td>RDS Proxy config; Lambda concurrency limit; alarm threshold</td></tr>
<tr><td>S3</td><td>customAuth Lambda timeout (currently 5 s)</td><td>Member sign-in latency; AppSync request P95</td><td>Whole dispatcher latency budget; PC sizing</td></tr>
<tr><td>S4</td><td>Number of PHI tables migrated per phase</td><td>Rollback complexity; cutover window length</td><td>Cutover playbook; rollback runbook</td></tr>
<tr><td>S5</td><td>Aurora minimum ACU (1.0)</td><td>Cold-start of database itself; idle cost; first-query latency</td><td>Cost table; reader/writer config; minimum-cost alarms</td></tr>
<tr><td>S6</td><td>CMK rotation policy (default 1 year)</td><td>Compliance posture; key version sprawl; envelope-encryption format</td><td>Field-encrypt/decrypt Lambdas; backups; audit-archive format</td></tr>
<tr><td>S7</td><td>CFN resource count per stack</td><td>Deploy reliability; per-PR feedback loop</td><td>Architecture decomposition; stack-split strategy</td></tr>
</tbody>
</table>
<h3>B.6.2 — Tradeoff points</h3>
<table>
<thead><tr><th>#</th><th>Tradeoff</th><th>Pulled toward</th><th>Document position</th></tr></thead>
<tbody>
<tr><td>T1</td><td><strong>Cost ↔ Performance:</strong> Aurora min ACU 1 (cheap, ~$87/mo) vs min ACU 2+ (faster cold-query, +$87/mo each step)</td><td>Cost — Aurora warms quickly; cold-query latency acceptable for non-PHI; first-call latency for PHI is handled by PC on dispatcher</td><td>§55 cost table; revisit if P95 first-query exceeds 1500 ms</td></tr>
<tr><td>T2</td><td><strong>Performance ↔ Compliance:</strong> Provisioned Concurrency hot-paths (cheap latency) vs zero PC (lower cost but more cold starts hit Cognito's 5 s ceiling)</td><td>Compliance — Phase 1 mandates PC for preTokenGen specifically because cold-start latency violates Cognito's hard 5 s timeout, which silently fails sign-in</td><td>§29; non-negotiable for the 5 hot Lambdas</td></tr>
<tr><td>T3</td><td><strong>Security ↔ Operability:</strong> RDS Proxy + IAM auth (1 round-trip per cold-start, plus dispatch tax) vs direct DB password from Secrets Manager (faster, but a password to rotate)</td><td>Security — IAM auth means we never store DB passwords; auditors love it; operability tax mitigated by PC + pgBouncer-like pooling in Proxy</td><td>§22; immovable</td></tr>
<tr><td>T4</td><td><strong>Modifiability ↔ Reliability:</strong> Track B incremental migration (low risk; long timeline) vs Track A 5-table consolidation (faster reads; high-risk schema change)</td><td>Reliability — Track B chosen specifically because incremental cutover with parallel old DDB during T-window allows 4 h rollback</td><td>Companion <code>CALL-PREP-UNIFIED.html</code>; §06 here</td></tr>
<tr><td>T5</td><td><strong>Cost ↔ Audit:</strong> S3 Object Lock Compliance mode (cannot delete; full HIPAA mode) vs Governance mode (can delete with audit log)</td><td>Audit — Compliance mode chosen; mistakes mean paying storage on garbage for 7 years; that's the right side to err on</td><td>§33; immovable</td></tr>
<tr><td>T6</td><td><strong>Single dispatcher ↔ Per-resolver Lambdas:</strong> One dispatcher (small attack surface, smaller team to review, but concentrates blast radius) vs per-resolver Lambdas (larger surface, parallel-deployable, but 37+ Lambdas to keep auth-correct)</td><td>Single dispatcher — see RT-6; concentration is correct given team size and audit budget</td><td>§06 + §B.5 RT-6</td></tr>
<tr><td>T7</td><td><strong>Cognito ↔ self-hosted IdP:</strong> External Cognito pool (existing; ext team owns it; RT-1 risk) vs Cognito pool owned by Childfree (faster iteration; but migration is another full project)</td><td>Existing Cognito — migration to a different IdP is out-of-scope for this plan; mitigate RT-1 with SLA</td><td>§59 question #2</td></tr>
</tbody>
</table>
<p><strong>Sensitivity points: 7. Tradeoff points: 7.</strong> All decisions documented above are positions the team takes <em>on purpose</em>; revisiting any of them is a re-architecture event, not a tweak.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.7 · WELL-ARCHITECTED =================================== -->
<!-- =============================================================== -->
<section id="well-arch">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.7</span>
<h2 style="border:0;margin:18px 0 0;">AWS Well-Architected — six-pillar self-scoring</h2>
</div>
<p>AWS Well-Architected Framework (Nov 2024 revision + April 2025 best-practice additions) defines 57 review questions across 6 pillars. Self-score below applied to the post-Phase-5 target architecture.</p>
<table>
<thead><tr><th>Pillar</th><th>Verdict</th><th>Strengths</th><th>Gap (closure in §B.11)</th></tr></thead>
<tbody>
<tr>
<td><strong>Operational Excellence</strong></td>
<td><span class="sev verified">STRONG</span></td>
<td>3 CloudWatch dashboards (§41); 16 alarms (§42); runbook for cutover + rollback; named on-call; pre/per-phase acceptance criteria (§58); Drizzle Kit in CI; release log</td>
<td>No GameDay schedule beyond DR drill. Closure: <strong>§B.11 #5</strong></td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td><span class="sev verified">STRONG</span></td>
<td>KMS CMK per concern (§20); RDS IAM auth (§22); RLS multi-tenancy (§07); customAuth Lambda fail-closed (§27); WAF v2 (§35); CloudTrail org trail (§34); audit-archive Object Lock Compliance (§33); Sentry scrubbing (§43); field-level encryption (§43); 15 pre-cutover gates (§53)</td>
<td>No GuardDuty / Macie / Security Hub explicitly stood up. Closure: <strong>§B.11 #6</strong></td>
</tr>
<tr>
<td><strong>Reliability</strong></td>
<td><span class="sev verified">STRONG</span></td>
<td>Aurora multi-AZ; reader endpoint (§09); RDS Proxy two target groups (§22); PITR 30d + S3 long-tail (§44); DR Drill quarterly (§45); rollback runbook + dress rehearsal × 2 (§47); deletion protection on PHI tables</td>
<td>No explicit multi-region DR (single-region snapshot copy only). Closure: <strong>§B.11 #7</strong> (and §59 Q13 already raises this for product decision)</td>
</tr>
<tr>
<td><strong>Performance Efficiency</strong></td>
<td><span class="sev warn">MEDIUM</span></td>
<td>PC for 5 hot Lambdas (§29); Aurora auto-scaling 1–8 ACU; reader for reporting (§09); dispatcher hot path; pgBouncer-via-RDS-Proxy</td>
<td>No formal load-test plan with target P95/P99 numbers. PC sized "by intuition." Closure: <strong>§B.11 #8</strong></td>
</tr>
<tr>
<td><strong>Cost Optimization</strong></td>
<td><span class="sev verified">STRONG</span></td>
<td>Cost table (§55); I/O-Optimized switch criteria (30% I/O / ACU ratio); ACU range 1–8; PC budget; AWS Cost Anomaly Detection (§42)</td>
<td>No Savings Plan / RI strategy for steady-state. Closure: <strong>§B.11 #9</strong></td>
</tr>
<tr>
<td><strong>Sustainability</strong></td>
<td><span class="sev high">WEAK</span></td>
<td>Auto-scaling implies right-sizing; Aurora Serverless reduces idle compute</td>
<td>Zero explicit Sustainability content. AWS now grades this; auditor will note absence. Closure: <strong>§B.11 #10</strong></td>
</tr>
</tbody>
</table>
<p><strong>Pillar score: 4 STRONG · 1 MEDIUM · 1 WEAK.</strong> The Sustainability pillar is the lowest score and the easiest win — adding a single subsection covering instance-rightsizing strategy, log retention tiering (already partially in §44), and Graviton2 selection for non-Aurora Lambdas closes it.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.8 · FITNESS FUNCTIONS ================================== -->
<!-- =============================================================== -->
<section id="fitness-functions">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.8</span>
<h2 style="border:0;margin:18px 0 0;">Fitness functions — automated architecture governance</h2>
</div>
<p>Per <em>Building Evolutionary Architectures</em> (Ford / Parsons / Kua, 2nd ed.): a fitness function is an automated, objective measure of an architectural characteristic, evaluated continuously. The 13 below transform this document's "we will…" promises into machine-enforced guards.</p>
<table>
<thead><tr><th>#</th><th>Fitness function</th><th>Target</th><th>Where it runs</th><th>Maps to</th></tr></thead>
<tbody>
<tr><td>FF1</td><td><strong>Tenant isolation matrix</strong> — every PHI table × every tenant role × CRUD = correct response</td><td>15 PHI tables × 4 roles × 4 ops = 240 assertions pass</td><td>CI integration suite (Phase 2 onward); on every dispatcher PR</td><td>§07 RLS · RT-6</td></tr>
<tr><td>FF2</td><td><strong>Fail-closed catch handlers</strong> — no <code>return event</code> from catch in <code>amplify/functions/</code></td><td>0 matches in grep</td><td>CI lint stage (Semgrep rule)</td><td>§53 · RT-2</td></tr>
<tr><td>FF3</td><td><strong>RLS denial rate</strong> — production dispatcher logs SQLSTATE 42501 events</td><td>= 0 over 24 h rolling</td><td>CloudWatch Logs Insights every 5 min</td><td>§42 alarm</td></tr>
<tr><td>FF4</td><td><strong>CFN resource count per stack</strong></td><td>Warn ≥ 400; fail ≥ 480; max 500 (AWS hard limit)</td><td>CI post-synth</td><td>RT-3</td></tr>
<tr><td>FF5</td><td><strong>Encryption at rest</strong> — all Aurora storage + S3 buckets use environment CMK (never aws/rds, aws/s3)</td><td>100% encrypted with environment CMK</td><td>AWS Config rules <code>rds-storage-encrypted</code>, <code>s3-default-encryption-kms</code></td><td>§20 · §51 §164.312(a)(2)(iv)</td></tr>
<tr><td>FF6</td><td><strong>PHI fields ↔ scrubber coverage</strong> — every <code>PHI: true</code> field in schema appears in Pino redaction + Sentry scrubber</td><td>100% coverage</td><td>CI unit test reads schema + scrubber configs and diffs</td><td>RT-5 · §43</td></tr>
<tr><td>FF7</td><td><strong>Heavy top-level imports</strong> — Sentry, AWS SDK, encryption SDK loaded at handler top-level in &gt; 10 Lambdas</td><td>≤ 5 (intentional list maintained)</td><td>CI AST scan</td><td>RT-4 · §30</td></tr>
<tr><td>FF8</td><td><strong>Tenant claim presence in JWT</strong> — every authenticated request to dispatcher has non-null trust_company_id</td><td>= 100% over 24 h</td><td>CloudWatch metric on customAuth Lambda</td><td>RT-2 · §27</td></tr>
<tr><td>FF9</td><td><strong>chil_admin / chil_migrator usage</strong> — break-glass and DDL roles used only during scheduled windows</td><td>0 unscheduled use over 30 d</td><td>CloudWatch Insights on pgaudit; alarms (§42)</td><td>§24 · §51 §164.312(a)(2)(ii)</td></tr>
<tr><td>FF10</td><td><strong>Lambda init duration P95</strong></td><td>&lt; 2000 ms across all 65+ Lambdas</td><td>CloudWatch Insights on Lambda Insights <code>init_duration</code></td><td>§42 alarm · §29 PC budget</td></tr>
<tr><td>FF11</td><td><strong>Audit non-repudiation coverage</strong> — every mutating dispatcher op writes audit_event in same tx</td><td>100% of writes vs <code>audit_event</code> count per 5 min &gt; 0.95 ratio</td><td>CloudWatch derived metric</td><td>§07 · §51 §164.312(b)</td></tr>
<tr><td>FF12</td><td><strong>CMK rotation enabled</strong> on all 4 environment CMKs</td><td>All 4 CMKs have rotation enabled</td><td>AWS Config managed rule <code>cmk-backing-key-rotation-enabled</code></td><td>S6 sensitivity · §51 §164.312(a)(2)(iv)</td></tr>
<tr><td>FF13</td><td><strong>CloudTrail integrity</strong> — Log File Validation passes</td><td>0 validation failures in 90 d</td><td>CloudTrail managed alarm (§42)</td><td>§34 · §51 §164.312(b)</td></tr>
</tbody>
</table>
<p><strong>13 fitness functions.</strong> 8 run in CI on every PR; 5 run continuously in production via CloudWatch / Config. Together they enforce every claim made in this document.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.9 · ADR INVENTORY ====================================== -->
<!-- =============================================================== -->
<section id="adr-inventory">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.9</span>
<h2 style="border:0;margin:18px 0 0;">ADR inventory — decisions that should be formalized</h2>
</div>
<p>Per Nygard / MADR / Y-Statement conventions: every architecturally significant decision deserves a record (Title · Status · Context · Decision · Consequences). The 11 below are the decisions <em>made</em> in this document but not yet extracted as ADRs. Phase 1 deliverable: write each as a one-page MADR file in <code>docs/adr/</code>.</p>
<table>
<thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Where decided in this doc</th></tr></thead>
<tbody>
<tr><td>ADR-001</td><td>Use Aurora Serverless v2 PostgreSQL (not RDS PostgreSQL, not DocumentDB, not Aurora Provisioned)</td><td>Accepted</td><td>§00 · §01</td></tr>
<tr><td>ADR-002</td><td>Use Postgres Row-Level Security (RLS) as the multi-tenancy spine, not application-layer policy</td><td>Accepted</td><td>§07</td></tr>
<tr><td>ADR-003</td><td>Custom dispatcher Lambda (single VPC-attached) over AppSync Aurora HTTP Resolver or per-resolver Lambdas</td><td>Accepted</td><td>§06 · RT-6 · T6</td></tr>
<tr><td>ADR-004</td><td>Five Postgres roles (chil_app, chil_sqlconnect, chil_reporter, chil_admin, chil_migrator) — not three, not seven</td><td>Accepted</td><td>§24</td></tr>
<tr><td>ADR-005</td><td>Drizzle Kit for migrations (dev-time only; pg driver at runtime, no ORM)</td><td>Accepted</td><td>§39</td></tr>
<tr><td>ADR-006</td><td>Track B incremental migration with Amplify <code>connect-to-SQL</code> + <code>a.combine()</code>, not Track A 5-table consolidation</td><td>Accepted</td><td>Companion <code>CALL-PREP-UNIFIED.html</code>; T4 here</td></tr>
<tr><td>ADR-007</td><td>RDS Proxy with two target groups (writer + reader) and IAM auth, not direct DB password</td><td>Accepted</td><td>§22 · T3</td></tr>
<tr><td>ADR-008</td><td>S3 audit-archive Object Lock <strong>Compliance</strong> (not Governance) for 7-year retention</td><td>Accepted</td><td>§33 · T5</td></tr>
<tr><td>ADR-009</td><td>Field-level encryption via AWS Encryption SDK envelope encryption (NOT pgcrypto in-database)</td><td>Accepted</td><td>§20 · §43</td></tr>
<tr><td>ADR-010</td><td>Cognito pool stays externally owned; integrate, do not migrate</td><td>Accepted</td><td>§26 · §59 Q2 · T7</td></tr>
<tr><td>ADR-011</td><td>Single VPC across environments via account separation, not per-tenant VPC</td><td>Accepted</td><td>§17 · §19</td></tr>
</tbody>
</table>
<p><strong>11 ADRs to write.</strong> Y-Statement form (single sentence: "In the context of X, facing Y, we decided for Z, because of W, accepting downside V") is fastest; full MADR (annotated form) for ADR-002, ADR-003, ADR-006 because those have the most consequences. Total effort: ~1 day for Oleksii.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.10 · MATURITY SCORECARD ================================ -->
<!-- =============================================================== -->
<section id="maturity">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.10</span>
<h2 style="border:0;margin:18px 0 0;">Architecture maturity scorecard</h2>
</div>
<p>Composite scoring across all seven review lenses. <strong>5 = world-class</strong>; <strong>3 = production-ready</strong>; <strong>1 = needs work</strong>.</p>
<table>
<thead><tr><th>Lens</th><th>Score (post-Phase-5 target)</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>arc42 documentation completeness</td><td><strong>4.0</strong></td><td>10/12 PASS once §B.3 + §B.9 close</td></tr>
<tr><td>C4 model visual decomposition</td><td><strong>4.0</strong></td><td>All 3 levels now present (§B.3); diagrams ASCII-only — Structurizr / Mermaid would lift to 5</td></tr>
<tr><td>ATAM quality attributes</td><td><strong>4.5</strong></td><td>18 scenarios with thresholds; phase gates explicit</td></tr>
<tr><td>ATAM risk surface</td><td><strong>4.0</strong></td><td>7 themes named + mitigated; RT-1 (external Cognito) remains residual</td></tr>
<tr><td>AWS Well-Architected</td><td><strong>4.0</strong></td><td>4 STRONG / 1 MEDIUM / 1 WEAK; Sustainability is the open pillar</td></tr>
<tr><td>Fitness functions / governance</td><td><strong>4.5</strong></td><td>13 functions cataloged; 100% automatable</td></tr>
<tr><td>ADR record discipline</td><td><strong>3.0</strong></td><td>11 ADRs to write; once written, lifts to 4.5</td></tr>
<tr><td>HIPAA + SOC 2 control mapping</td><td><strong>4.5</strong></td><td>Every safeguard mapped to a phase + verifier; auditor evidence pack designed</td></tr>
<tr style="background:var(--accent-soft);font-weight:700;"><td><strong>Composite</strong></td><td><strong>4.1 / 5</strong></td><td>"Production-ready, with named gap-closure items"</td></tr>
</tbody>
</table>
<p>The 0.9 deficit lives almost entirely in arc42-style documentation polish and ADR-record discipline — both closable in &lt; 1 week of Oleksii's time as part of Phase 1.</p>
</section>
<hr>
<!-- =============================================================== -->
<!-- ===== B.11 · GAP CLOSURE ====================================== -->
<!-- =============================================================== -->
<section id="gap-closure">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;">
<span class="section-num">Appendix · B.11</span>
<h2 style="border:0;margin:18px 0 0;">Pre-Phase-1 gap closure deliverables</h2>
</div>
<p>Every gap identified above maps to one of these 10 deliverables. None is large. All are owned by Oleksii unless noted. All produce artifacts that go into the auditor evidence pack (§54).</p>
<table>
<thead><tr><th>#</th><th>Deliverable</th><th>Closes</th><th>Effort</th><th>Owner</th></tr></thead>
<tbody>
<tr><td>1</td><td><strong>Constraints register</strong> — one-page MD listing all hard constraints (CFN 500, ENI 250, Cognito 5 s, BAA-eligible services list, US-only data residency, etc.)</td><td>arc42 §2</td><td>2 h</td><td>Oleksii</td></tr>
<tr><td>2</td><td><strong>Sequence diagrams</strong> for PHI read with RLS · cutover flip · 4 h rollback. Mermaid in repo</td><td>arc42 §6 Runtime View</td><td>4 h</td><td>Oleksii</td></tr>
<tr><td>3</td><td><strong>11 ADRs</strong> in <code>docs/adr/</code> per §B.9; MADR for ADR-002/3/6, Y-Statement for the rest</td><td>arc42 §9 / Decision discipline</td><td>1 day</td><td>Oleksii</td></tr>
<tr><td>4</td><td><strong>Quality scenarios test harness</strong> — codify §B.4 utility tree as 18 named assertions in CI</td><td>arc42 §10; Fitness function coverage</td><td>2 days</td><td>Oleksii + Mike</td></tr>
<tr><td>5</td><td><strong>GameDay schedule</strong> — quarterly chaos exercises beyond DR Drill (failed-claim test, RLS-bypass attempt, dispatcher OOM, cold-Cognito test)</td><td>Well-Architected Operational Excellence</td><td>1 day to design; recurring</td><td>Mike</td></tr>
<tr><td>6</td><td><strong>GuardDuty + Macie + Security Hub</strong> stand-up with HIPAA + SOC 2 standard packs</td><td>Well-Architected Security; SOC 2 CC7</td><td>1 day</td><td>Mike</td></tr>
<tr><td>7</td><td><strong>Multi-region DR decision &amp; design</strong> — answer §59 Q13; if YES, design cross-region snapshot copy + DNS failover</td><td>Well-Architected Reliability</td><td>0.5 day decision + 2 days implementation if YES</td><td>Mike + Jon</td></tr>
<tr><td>8</td><td><strong>Load test plan</strong> with target P95/P99 numbers per scenario (§B.4) · execute in dress rehearsal</td><td>Well-Architected Performance Efficiency</td><td>2 days plan + 1 day exec</td><td>Mike</td></tr>
<tr><td>9</td><td><strong>Savings Plan / Reserved Instance analysis</strong> on steady-state Aurora + Lambda usage; commit if &gt; 30% savings</td><td>Well-Architected Cost</td><td>0.5 day analysis</td><td>Mike</td></tr>
<tr><td>10</td><td><strong>Sustainability subsection</strong> — Graviton2 for non-VPC Lambdas; CloudWatch log retention tiering (already in §44); document idle-ACU savings; commit to instance-rightsizing reviews quarterly</td><td>Well-Architected Sustainability</td><td>2 h documentation + 0.5 day Graviton migration</td><td>Mike</td></tr>
</tbody>
</table>
<p><strong>Total gap-closure effort: ~10 person-days.</strong> All fit inside Phase 0–1. Each produces a tangible artifact (file in the repo, dashboard, runbook entry, or test in CI) that the auditor can verify exists.</p>
<div class="callout ok">
<div class="callout-label">Post-closure self-score</div>
With deliverables 1–10 complete, the composite maturity score moves from <strong>4.1 → 4.7 / 5.0</strong>. The remaining
0.3 deficit lives in residual external dependencies (Cognito pool ownership, RT-1) and in ADRs that cannot be written
until ADR-002/3/6 ship and prove out in Phases 2–4.
</div>
</section>
<footer>
Childfree Legacy — Aurora Migration: Complete Plan + DevOps Deep Review + Architecture Review · Internal · 2026-05-26<br>
Companion to <code>CALL-PREP-UNIFIED.html</code> (diagnosis + Track A alternative + full decision context)<br>
Code-validated against current main <code>ecd31d25b</code> · Architecture review applied: arc42 · C4 · ATAM · AWS Well-Architected · Fitness functions · MADR · HIPAA/SOC 2 mapping<br>
Owner: Jon · Mike (DevOps) · Oleksii (Data Architect)
</footer>
</main>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment