Security13 min read

HIPAA-Compliant Clinical Platform: The Architecture Decisions That Define What's Possible Later

What a domain-expert founder building a healthcare app actually needs to know before writing a line of code: covering PHI isolation, audit trails, AI integration, and the MVP order of operations that keeps compliance debt from compounding.

Liban Abdullahi·
126 views
A clean layered diagram: patient/clinician UI → API layer → PHI-isolated data store → audit log, with a "BAA boundary" callout. 1200×630

The founders we work with in healthcare mostly arrive with the same profile: deep domain knowledge, a clear clinical problem, and an early-stage product that urgently needs to exist. What they almost always lack is a map of which technical decisions are load-bearing for compliance, and which ones, made quickly and innocently in the MVP phase, compound into a rewrite before the first real patient.

This post is that map.

We've built HIPAA-adjacent systems across clinical operations platforms, AI-assisted diagnostic tools, and healthcare data pipelines. The pattern of what breaks (and when) is consistent. The decisions below are not theoretical; they're the ones we revisit in every engagement where someone comes in with a working demo and a compliance problem.

TL;DR: Build in this order: (1) auth + RBAC + session management, (2) PHI-isolated data layer with encryption at rest, (3) append-only audit log, (4) the clinical feature, (5) AI inference with a de-identification boundary. Never send raw PHI to an LLM without a BAA. Get vendor BAAs signed before any PHI touches the stack, not after. A 42-column users table with PHI mixed into operational data is the most common source of a rewrite.

What HIPAA actually requires of your architecture

HIPAA's Security Rule doesn't specify technologies. It specifies outcomes: PHI must be accessible only to authorized users, every access must be auditable, and breaches must be detectable and notifiable within 60 days. The implementation choices are yours.

In practice, that translates to four architectural invariants:

  1. PHI is identified and isolated: you know exactly which fields contain Protected Health Information and where they live. You can't de-identify what you can't locate.
  2. Access is role-based and auditable: every PHI read or write is attached to an authenticated identity, a role, and a timestamp. This log must be immutable and retained for six years.
  3. Data is encrypted in transit and at rest: TLS ≥1.2 everywhere; AES-256 or equivalent for storage.
  4. Your vendors have signed BAAs: every service that touches PHI (database, object storage, email, LLM API) must have a Business Associate Agreement in place. This is contractual, not technical, but it's a hard blocker on production.

None of those are hard to implement. The problem is when they're retrofitted onto a codebase that was designed without them: PHI scattered across six tables, no audit log, a vendor stack selected for speed without checking BAA availability.

The PHI isolation decision (and why it's first)

The single highest-leverage decision in a clinical platform is: where does PHI live, and how is it separated from everything else?

The typical MVP mistake is a flat users table:

-- The table that causes the rewrite
CREATE TABLE users (
  id           UUID PRIMARY KEY,
  email        TEXT,
  created_at   TIMESTAMPTZ,
  -- ... operational fields ...
  full_name    TEXT,       -- PHI
  date_of_birth DATE,      -- PHI
  diagnosis_codes TEXT[],  -- PHI
  clinical_notes TEXT      -- PHI
);

This works until you need to: run analytics on user behaviour (now your analytics pipeline touches PHI), add a third-party integration (now every integration is a BAA requirement), export a row for debugging (now every developer has seen PHI), or de-identify data for a research partner.

The pattern we use instead: a PHI schema isolated from the operational schema, with its own access controls and encryption layer.

-- Operational schema: no PHI, safe for analytics and integrations
CREATE TABLE accounts (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email       TEXT NOT NULL UNIQUE,
  role        TEXT NOT NULL CHECK (role IN ('clinician','admin','patient')),
  created_at  TIMESTAMPTZ DEFAULT now()
);

-- PHI schema: isolated, encrypted at rest, access-logged
CREATE SCHEMA phi;

CREATE TABLE phi.patient_records (
  account_id      UUID REFERENCES accounts(id),
  full_name       BYTEA NOT NULL,    -- field-level encrypted
  date_of_birth   BYTEA NOT NULL,
  clinical_notes  BYTEA,
  updated_at      TIMESTAMPTZ DEFAULT now()
);

Field-level encryption means PHI at rest is ciphertext even if the database host is compromised. The application layer holds the encryption key (or delegates to a KMS), so a database dump is useless without the application's key material.

On PostgreSQL this is straightforward with pgcrypto:

-- Write: encrypt on insert
INSERT INTO phi.patient_records (account_id, full_name, date_of_birth)
VALUES (
  $1,
  pgp_sym_encrypt($2, $KEY),
  pgp_sym_encrypt($3::text, $KEY)
);

-- Read: decrypt on select
SELECT
  pgp_sym_decrypt(full_name, $KEY) AS full_name,
  pgp_sym_decrypt(date_of_birth, $KEY)::date AS date_of_birth
FROM phi.patient_records
WHERE account_id = $1;

In practice we move the key management to the application layer (Python cryptography library, or a cloud KMS) rather than passing the key into SQL, but the schema boundary is what matters here, regardless of where you implement the encryption.

The benefit of the PHI schema boundary: every ORM query, every migration, every log line that touches phi.* is immediately visible as PHI-touching. You can grep for it. You can set stricter database user permissions on that schema. Your analytics pipeline never needs GRANT SELECT ON phi.*.

Auth, RBAC, and session management (before the feature)

Build the auth layer before building the clinical feature. This sounds obvious; it almost never happens in practice.

The minimum HIPAA-compliant auth stack for a clinical platform:

RequirementWhy it's non-negotiable
Role-based access controlClinicians, admins, and patients have different PHI access. This must be enforced at the API layer, not the UI.
MFA for PHI accessHIPAA doesn't mandate MFA by name, but a successful PHI breach without MFA is very difficult to defend against the "addressable" safeguard standard.
Session timeoutInactive sessions must expire. HIPAA references "automatic logoff" explicitly. 15 minutes of inactivity is a common baseline.
Failed login trackingLockout after repeated failures; log the attempts.

In a Django + Next.js stack (which is what we typically build on), this looks like:

# Middleware-level PHI access enforcement
class PHIAccessMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if self._is_phi_endpoint(request.path):
            if not request.user.is_authenticated:
                return JsonResponse({'error': 'authentication required'}, status=401)
            if not request.user.has_mfa_verified:
                return JsonResponse({'error': 'mfa required for phi access'}, status=403)
            if not self._session_fresh(request):
                return JsonResponse({'error': 'session expired'}, status=401)
        return self.get_response(request)

    def _session_fresh(self, request):
        last_activity = request.session.get('last_activity')
        if not last_activity:
            return False
        return (time.time() - last_activity) < 900  # 15 minutes

The RBAC decision that compounds if wrong: enforce roles at the API layer, not the frontend. A UI that hides a button is not access control. Every PHI endpoint must check request.user.role server-side before returning data.

The audit log (immutable, from day one)

HIPAA requires a six-year audit trail of PHI access. More practically: if something goes wrong (a breach, a complaint, a regulator inquiry), the audit log is the only thing that lets you answer "who saw what, and when."

The common MVP shortcut is updated_at and created_at columns. Those tell you when a record changed, not who touched it. They're also mutable; a developer can UPDATE them directly.

The pattern that works: a separate, append-only audit table that no application user can delete from or update.

CREATE TABLE audit.phi_access_log (
  id          BIGSERIAL PRIMARY KEY,
  account_id  UUID NOT NULL,            -- who
  action      TEXT NOT NULL,            -- 'read' | 'write' | 'delete'
  resource    TEXT NOT NULL,            -- 'phi.patient_records'
  record_id   UUID,                     -- which record
  ip_address  INET,
  user_agent  TEXT,
  occurred_at TIMESTAMPTZ DEFAULT now() -- when
);

-- Revoke delete and update from the application DB user
REVOKE DELETE, UPDATE ON audit.phi_access_log FROM app_user;

The application writes to this table on every PHI access. In Django, a simple mixin handles it:

class PHIAuditMixin:
    def retrieve(self, request, *args, **kwargs):
        response = super().retrieve(request, *args, **kwargs)
        PHIAccessLog.objects.create(
            account_id=request.user.id,
            action='read',
            resource=self.queryset.model.__name__,
            record_id=kwargs.get('pk'),
            ip_address=get_client_ip(request),
            user_agent=request.META.get('HTTP_USER_AGENT', ''),
        )
        return response

For additional tamper-resistance, ship the audit log off-host: to S3 (with object lock), to an append-only log aggregator, or to a SIEM like Wazuh. The goal is that even a compromised application server cannot erase its access history.

Business Associate Agreements before any PHI

This is contractual, not technical, but it's a hard blocker: every vendor whose infrastructure touches PHI must sign a BAA before PHI goes in.

The stack-level checklist for a typical Next.js + Django deployment:

ServiceBAA available?Notes
AWS (RDS, S3, EC2)YesVia AWS console; covers the specific services you list
GCP / AzureYesSimilar process
VercelNo (as of mid-2026)Don't send PHI to the frontend host; keep it API-only
Supabase CloudNo standard BAASupabase self-hosted or alternatives with BAA needed
OpenAI APINoCannot send raw PHI
Azure OpenAIYesDe-identification still best practice regardless
TwilioYesFor PHI-touching SMS/email (e.g., appointment reminders)
SendGridYesSeparate BAA, covers email
StripeGenerally not neededPayment data is not PHI

The one that surprises founders most: OpenAI does not sign BAAs. If your AI feature involves patient data and you're calling the OpenAI API directly, you have a compliance problem regardless of how the API key is stored.

AI integration: the de-identification boundary

Clinical AI features almost always involve PHI: patient notes, diagnoses, lab results. The architecture question is: how does PHI get to (or not get to) the model?

The pattern that keeps you compliant:

Patient data (PHI)
      │
      ▼
De-identification layer        ← strip or replace: names, dates, IDs, locations
      │
      ▼
LLM inference (de-identified)  ← OpenAI, Claude, or self-hosted (Ollama)
      │
      ▼
Re-identification layer        ← map output back to the patient record
      │
      ▼
Structured result stored with PHI

The de-identification step doesn't need to be perfect NLP. For most clinical platforms it's sufficient to:

  1. Replace patient names and identifiers with tokens ([PATIENT_A], [PATIENT_B])
  2. Shift or remove dates
  3. Replace geographic specifics (clinic names, city names) with generics

This gets you to a state where the LLM call can use OpenAI or any other provider without BAA concerns, because the payload no longer contains PHI.

def deidentify_clinical_note(note: str, record_id: str) -> tuple[str, dict]:
    """Returns (deidentified_text, entity_map) for re-identification."""
    entity_map = {}

    # Replace patient identifiers
    for pattern, label in IDENTIFIER_PATTERNS:
        matches = re.findall(pattern, note)
        for match in matches:
            token = f"[{label}_{len(entity_map)}]"
            entity_map[token] = match
            note = note.replace(match, token)

    return note, entity_map


def reidentify_output(text: str, entity_map: dict) -> str:
    for token, original in entity_map.items():
        text = text.replace(token, original)
    return text

If you need richer NLP-level de-identification (detecting names the rule-based approach misses), AWS Comprehend Medical has a dedicated PHI detection API and a BAA, though it adds per-call cost and latency.

For platforms where PHI must stay entirely on-premises (some clinical environments require this), a self-hosted model via Ollama sidesteps the BAA question entirely at the cost of hardware and inference quality.

The MVP build order

Most clinical platform MVPs are built in this order:

  1. The clinical feature (demo-able, investor-friendly)
  2. Auth (bolted on when the first real user needs it)
  3. Compliance (bolted on when the first enterprise client asks)

The order that avoids a rewrite:

  1. Auth + RBAC + session management: before any data layer
  2. PHI schema with field-level encryption: before any patient data enters the system
  3. Audit log: before the first real patient record
  4. Vendor BAAs signed: before production traffic
  5. The clinical feature: now built on a compliant foundation
  6. AI inference path: with de-identification boundary if applicable

The key insight: steps 1–4 are mostly one-time setup. They take a week or two done properly. The feature (step 5) takes as long as it takes regardless of order. The only thing the wrong order buys you is a working demo that you can't ship to real patients without rebuilding the data layer.

Deployment: what HIPAA-eligible means on AWS

AWS is the most common choice for HIPAA-compliant hosting. A few specifics:

Which AWS services are HIPAA-eligible is defined in the AWS HIPAA whitepaper. The key ones for a typical stack: RDS, EC2, S3, Lambda, Cognito, Secrets Manager, CloudTrail, CloudWatch. Notably, some services are not on the list; always verify before adding a new AWS service to your PHI data path.

CloudTrail must be on. This is AWS's native audit log for API-level actions (who called what API, when, from where). It's not a substitute for your application-level PHI audit log, but it covers infrastructure actions (who stopped the database, who accessed the S3 bucket). Enable it in all regions; store the logs in a separate account or with Object Lock.

Secrets Manager over environment variables for credentials. HIPAA requires access controls on PHI and on the credentials that access PHI. Hard-coded secrets in a .env file don't have access controls. Secrets Manager does, and it rotates automatically.

# Fetch PHI encryption key from Secrets Manager at runtime
import boto3

def get_phi_key() -> str:
    client = boto3.client('secretsmanager', region_name='eu-west-1')
    response = client.get_secret_value(SecretId='prod/phi-encryption-key')
    return response['SecretString']

Subnets and security groups. The database should be in a private subnet with no route to the internet. The only inbound path to RDS should be from the application server's security group, on the database port. Nothing else.

Frequently asked questions

Does a clinical platform MVP need all of this on day one? It needs the data layer and auth before any real patient data enters the system. The audit log should be in before the first clinical workflow is live. BAAs must be in place before production. The AI de-identification layer matters only once you're calling an LLM with patient context. A pure demo with synthetic data can skip everything, but that's not a production system.

What's the difference between HIPAA-eligible and HIPAA-compliant hosting? A hosting provider being "HIPAA-eligible" means they'll sign a BAA and their infrastructure meets the technical safeguards. HIPAA compliance is your responsibility as the covered entity or business associate: it's about how you configure and use the infrastructure, not just who you host with. A HIPAA-eligible host with a misconfigured database and no audit log is not a compliant deployment.

Can I use Supabase or Vercel for a HIPAA-compliant clinical app? As of mid-2026: Supabase Cloud and Vercel do not offer BAAs as a standard product. Self-hosted Supabase on your own AWS infrastructure is an option. For the Next.js frontend, the rule is simpler: PHI should never reach the frontend host; keep it in the API layer, behind auth, and your frontend host becomes a non-PHI service.

Is field-level encryption necessary if the database is already encrypted at rest? Database-at-rest encryption (the default on RDS) protects the disk if AWS loses physical custody of the storage hardware. It doesn't protect you if the database credentials are compromised; an attacker with credentials can query plaintext. Field-level encryption means the data is ciphertext in the database, so a credential compromise yields encrypted blobs without the application key. Both layers are worth having.

What if the AI model needs to reason over patient history (full clinical context)? For models that need full patient context, either: (a) use Azure OpenAI, which offers a BAA and processes data under enterprise privacy terms, or (b) run a self-hosted model (Ollama with a capable open model) so PHI never leaves your infrastructure. The de-identification approach works well for narrow tasks (classification, note structuring) but is harder for open-ended clinical reasoning where the model's answer depends on exact patient details.

How long does a HIPAA-compliant MVP actually take to build? The compliance scaffolding (steps 1–4 above) adds roughly one to two weeks to a typical MVP timeline. That's the setup cost, not a per-feature overhead. The bigger time driver is complexity of the clinical workflow itself and the number of PHI data types in scope. A well-scoped first release (one clinical user type, one workflow, narrow PHI surface) can ship in eight to twelve weeks built to a compliant architecture.

Wrapping up

The architectural pattern is consistent across every compliant clinical platform we've built: PHI isolated from the start, auth and roles before features, audit log before real patients, AI inference with a de-identification boundary. None of these are expensive to implement if they go in early. All of them are expensive to retrofit.

If you're a clinician or domain expert building something in a regulated space and you're evaluating technical approaches: the thing most worth getting right first is the data model. Everything else is reversible.

Building a clinical platform or healthcare tool and not sure whether your current architecture will survive a compliance review? Get in touch. We are happy to take a look.

Tags

#security#hipaa#healthcare#clinical-platform#mvp#phi#compliance#architecture#ai#postgresql#nextjs#django