HIPAA Compliance
HIPAA Compliance Scaffold
HIPAA compliance is scaffolded into the architecture from day one. This is not an afterthought — every vendor choice, logging decision, and data flow is designed with the Security Rule and Privacy Rule in mind.
The Trust Zone (organizing principle)
Define one trust zone. A vendor may touch PHI only if it is inside it — i.e. we hold a signed BAA and the service is configured for it.
- PHI at rest lives only in Neon, GCS, Vertex AI, and WellSky, plus the governed Microsoft 365 plane.
- PHI in motion only crosses services that are inside the zone.
- Anything outside the zone receives references (opaque IDs), never PHI.
The governed Microsoft 365 plane holds PHI in practice. The workforce plane (Microsoft 365 — Teams, SharePoint, OneDrive, Exchange) will contain PHI (e.g. Excel/Word files with patient info), so it is treated as a governed plane under a Microsoft BAA, with Purview labeling/DLP + Intune device control + Entra Conditional Access — the control is policy about where PHI lives and who accesses it, not pretending PHI stays out. By contrast, Notion is NOT HIPAA-covered (no BAA) — no PHI of any kind in Notion. See Business Operations.
🚫 PHI-restricted services (opaque IDs by default)
Default posture: opaque identifiers only. The services below receive opaque IDs — never names, MRNs, DOBs, or clinical notes. A few carry a specific, stated carve-out (Restate holds KMS-ciphertext; PostHog is BAA-covered for behavioral events) — spelled out in their row. Everywhere else, code fetches PHI from Neon inside the trust zone at the point of use.
| Service | Role | Rule |
|---|---|---|
| Durable execution — Restate Cloud (managed) | Async / agent orchestration | No PHI shared with Restate Cloud (initial posture). Any PHI that must be journaled is encrypted at the serde boundary inside the Cloud Run handler (GCP-KMS-backed encrypting Serde), so Restate Cloud's journal and virtual-object state hold ciphertext only; the primary entity identifiers are opaque, short-lived IDs, so keys, trace IDs, and call structure stay PHI-free and non-correlatable. Restate Cloud is an encrypted conduit, so no BAA is required. Keep object keys, state-key names, and error messages PHI-free. Simple jobs pass opaque short-lived IDs and re-fetch PHI from Neon in-zone. |
| PostHog | Product analytics / flags / experiments / session replay / error tracking / LLM observability + evals | BAA-covered carve-out (PostHog Cloud, Boost+ platform package, self-serve BAA): behavioral events are permitted under the BAA, but identify users by opaque IDs, put no PHI in flag keys, event names, or property keys, enable PHI masking on session replay, and scrub PHI from captured error payloads and LLM prompt/response traces. |
| Sanity | Marketing / CMS content | Marketing + in-app copy only. Never store PHI. |
| Neon Data API (PostgREST) | Alt data access | Outside the HIPAA boundary — no PHI path may use it. Use a direct Postgres connection for anything touching PHI. |
| Telnyx SMS bodies | Notifications / 2FA | Keep PHI out of message text (SMS is not end-to-end encrypted on the carrier network). Clinical detail stays on the voice channel. |
| Notion (docs & wiki — incl. the architecture plan itself) | Planning & documentation | No BAA — Notion does not sign one; it is not HIPAA-covered. Zero PHI of any kind — none in pages, comments, tickets, screenshots, pasted logs, or examples. Synthetic / de-identified data only. |
| Sales & internal tools — Clay, ReachInbox, Microsoft Teams, Linear, Notion | CRM / comms / tickets / notes / meeting notes | Never paste PHI into outreach tools, chat, tickets, wikis, or meeting notes. |
"In the boundary" ≠ "OK to log PHI." Cloud Logging, Cloud Trace, and OTel spans are covered by the Google BAA, but PHI must still be scrubbed before it is written there — see Observability.
Vendor BAAs Required
Every vendor that could handle Protected Health Information (PHI) requires a Business Associate Agreement:
| Vendor | Plan / Config | BAA |
|---|---|---|
| GCP (Cloud Run, BigQuery, GCS, Secret Manager) | Standard | ✅ Google BAA — verify project coverage |
| Neon | Scale plan; set hipaa:true per project (irreversible; audit_log_level=hipaa) | ✅ Self-serve BAA |
| Vertex AI (Gemini) | Reasoning, embeddings, voice; pin to a US region | ✅ Under the Google BAA — request ZDR / abuse-logging opt-out. Never use ai.google.dev. See AI Tier |
| PostHog (product analytics, flags, experiments, session replay, error tracking, LLM observability + evals) | PostHog Cloud on the Boost+ platform package; enable session-replay PHI masking + scrub error/LLM payloads | ✅ Self-serve BAA at app.posthog.com/legal — BAA lands on Boost+. Keep PHI out of flag keys, event names, property keys, error payloads, and LLM traces |
| WellSky | Core EHR / referral management / e-fax / Family Room — holds real PHI | ✅ BAA required (core PHI system). WellSky↔Neon sync runs in backend/worker |
| AWS SES | Transactional (product) email; us-west-2, co-located with Neon | ✅ Free self-serve AWS BAA — no PHI in email bodies |
Do not store or process PHI with any vendor until the BAA is signed and confirmed.
Durable execution — Restate Cloud (managed, locked). Restate runs on Restate Cloud (managed, serverless, us region) driving Cloud Run handlers. The initial posture shares no PHI with Restate Cloud — there is no Restate BAA. Because Restate journals ctx.run results, call args/results, awakeable payloads, and virtual-object state (ctx.set), any PHI that must be journaled is encrypted at the serde boundary inside the handler via a GCP-KMS-backed encrypting Serde — Restate Cloud's journal and state hold ciphertext only, never plaintext. The serde does not encrypt keys, so the primary entity identifiers are opaque, short-lived IDs (not stable DB primary keys): object keys, state-key names, error messages, and trace IDs stay PHI-free and non-correlatable. Restate Cloud thus handles only ciphertext + PHI-free metadata, so it is treated as an encrypted conduit and no vendor BAA is required. Sharing PHI with Restate later would require a signed BAA first.
Authorization — Cerbos (self-hosted, locked). Cerbos is a stateless policy-decision-point running self-hosted in GCP (Cloud Run / sidecar container). It evaluates policy-as-code (YAML + CEL) over opaque IDs, roles, and attributes only — its inputs and decisions are PHI-free, so no vendor BAA is required. It holds no data at rest. Neon RLS remains the enforcement floor; Cerbos's Query Plan API emits SQL WHERE filters that pair with RLS. See Backend.
Work in progress — the voice / telephony vendor is not locked.
- Voice / telephony agent (favored: self-hosted Pipecat + Telnyx): Telnyx BAA for the carrier; the brain + speech run on Gemini Live API + Chirp STT/TTS (Vertex, under the GCP BAA) per the AI Tier decision. Everything in-boundary.
See the Compute Stack Plan (Notion) for the live evaluation.
Technical Controls
| Control | Status | Details |
|---|---|---|
| PHIScrubFilter | Stub (must implement) | Attached to root logger; blocks all logs until PHI scrubbing is implemented |
| Secrets | Must configure | GCP Secret Manager (runtime) + Pulumi ESC (declarative). No DB-resident vault. See Secret Management |
| Neon RLS | Must enable | Row-Level Security on all tables containing patient data; schema + policies defined in the top-level schema/ dir (Drizzle Kit); policies key on auth.user_id() via pg_session_jwt (Better Auth JWTs) |
| BigQuery access controls | Must review | Restrict dataset access to authorized service accounts; de-identify PHI in the warehouse or keep the dataset inside the BAA |
| Cloud Run service account | Must configure | Least-privilege principle |
| Cloud Run env vars | Must review | No PHI in build logs or container env |
| Audit logging | Must enable | Neon audit_log_level=hipaa; GCP Cloud Audit Logs |
Data Handling
Storage
- Neon: storage + compute billed as usage on the Scale plan; monitor branch/compute usage.
- BigQuery: first 10 GB storage free; ~$0.02/GB/month after. Review partitioning at 100 GB.
- GCS: object storage for uploads/files — add when the first upload feature lands.
Cross-Cloud Data Transfer
- The analytics sync flows Neon (OLTP, source of truth) → BigQuery (OLAP), incrementally (
WHERE updated_at >) to minimize egress across the GCP ↔ AWS boundary (Neon runs in AWSus-west-2, same metro as Cloud Runus-west1). - Full-table cross-cloud scans are prohibited — they incur significant egress cost and are a compliance risk.
PHI in Logs
PHI must never appear in:
stdout/stderron Cloud Run- Cloud Logging / Log Analytics
- Cloud Build / Cloud Run logs
- GitHub Actions logs
- Any observability pipeline (Cloud Trace spans, OTel)
Enforced by the PHIScrubFilter in the Python backend services (backend/api and backend/worker) and manually audited in frontend code.
Compliance Checklist
A hipaa-checklist.md file is included in every project with the full actionable checklist. Teams should review and check off each item before handling production PHI.
See also:
- Observability — PHI scrub filter details
- Infrastructure — Neon setup with BAA
- Secret Management — GCP Secret Manager + Pulumi ESC
- Environment Variables — Secret storage locations