← Back to VHScanner Atlas

HIPAA Compliance & Data Storage

How to store scan results, stay compliant with health data regulations, and protect user privacy at every stage.

Does HIPAA Apply to Us?
Short answer: maybe not yet, but build for it anyway

HIPAA applies to covered entities (hospitals, doctors, health plans, insurers) and their business associates (anyone who handles PHI on their behalf). A standalone consumer wellness app that doesn't integrate with healthcare providers is technically NOT a covered entity.

But here's what DOES apply to you right now:
FTC Health Breach Notification Rule — applies to ALL health apps and personal health records, even if HIPAA doesn't. If you have a breach involving health data, you must notify affected users and the FTC. Penalties are real.

When HIPAA kicks in:

  • You partner with a clinic, doctor's office, or hospital that sends patients to your app
  • An employer uses your scanner for employee wellness programs
  • An insurance company integrates your scan data into their risk assessment
  • A practitioner uses your platform to track patient health over time
Why build for HIPAA anyway
Building HIPAA-compliant from day one is 10x cheaper than retrofitting. It's also a competitive advantage — "HIPAA-compliant" on your marketing materials builds trust instantly. And when that first clinic or employer comes knocking, you're ready.
🛠
Two Compliant Architectures
Client-side only vs server-side storage — pick based on your product needs
When You Need Cloud

Option B: Server-Side Storage

For scan history, practitioner sharing, and analytics. Requires full HIPAA compliance.
  • Scan history across devices
  • Share results with practitioners
  • Longitudinal health tracking
  • Population analytics (anonymized)
  • Requires BAA with all vendors
  • Requires encryption at rest + in transit
  • Requires audit logging
  • Requires breach notification plan
💡
Start with Option A for MVP. Zero HIPAA burden. User's data stays on their phone. Add "Export to PDF" and "Save locally." Move to Option B when you add practitioner sharing or need longitudinal tracking.
📱
Option A: Client-Side Architecture
How to store scan results on-device with zero server exposure
Phone (Client-Side Only)
┌─────────────────────────────────────────────┐
│                                             │
│  Camera/Sensors                             │
│       │                                     │
│       ▼                                     │
│  MediaPipe / ONNX / Web Audio               │
│       │                                     │
│       ▼                                     │
│  346 Marker Results (processed locally)     │
│       │                                     │
│       ├──▶ IndexedDB (encrypted)            │
│       │    └─ AES-256 with user passphrase  │
│       │                                     │
│       ├──▶ Export to PDF (user-initiated)    │
│       │                                     │
│       └──▶ Export to JSON (for backup)      │
│                                             │
│  YOUR SERVERS NEVER SEE THIS DATA            │
└─────────────────────────────────────────────┘

Implementation Details

StorageMethodCapacityEncryption
Scan ResultsIndexedDBUnlimited (browser-managed)Web Crypto API — AES-256-GCM with user-derived key
User ProfileIndexedDBSmall (<10KB)Same encryption key
Scan HistoryIndexedDB~34KB per scan × N scansSame encryption key
SettingslocalStorage<1KBNot needed (no PHI)

Client-Side Encryption Code Pattern

// Derive encryption key from user's passphrase
async function deriveKey(passphrase: string): Promise<CryptoKey> {
  const enc = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: enc.encode('vhscanner-salt'), iterations: 100000, hash: 'SHA-256' },
    keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
  );
}

// Encrypt scan results before storing
async function encryptResults(data: object, key: CryptoKey): Promise<{iv: Uint8Array, data: ArrayBuffer}> {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, key, new TextEncoder().encode(JSON.stringify(data))
  );
  return { iv, data: encrypted };
}

// Store in IndexedDB
async function saveScan(scanResults: MarkerResult[], passphrase: string) {
  const key = await deriveKey(passphrase);
  const { iv, data } = await encryptResults(scanResults, key);
  // Store iv + data in IndexedDB — even if someone accesses the DB, they can't read it
}
Option B: Server-Side Architecture
Supabase + encryption for cloud storage with full HIPAA compliance
Phone (Client-Side)
┌─────────────────────────────────────────┐
│  Camera → Processing → 346 Markers      │
│       │                                 │
│       ▼                                 │
│  ENCRYPT with user's key BEFORE sending  │
└───────┬─────────────────────────────────┘
        │ HTTPS (TLS 1.3)
        ▼
Supabase (Pro Plan + BAA signed)
┌─────────────────────────────────────────┐
│                                         │
│  Auth: Supabase Auth (email/OAuth)      │
│  RLS: user can ONLY read own rows       │
│                                         │
│  Table: scans                          │
│   - id (uuid, PK)                       │
│   - user_id (FK → auth.users)           │
│   - scan_date (timestamptz)             │
│   - scan_types (text[])                 │
│   - encrypted_results (bytea) ← AES    │
│   - iv (bytea)                          │
│   - result_hash (text) ← integrity     │
│   - created_at (timestamptz)            │
│                                         │
│  Table: scan_access_log  (audit trail)  │
│   - id, user_id, action, ip,           │
│     user_agent, accessed_at             │
│                                         │
│  NO raw images                          │
│  NO video frames                        │
│  NO PII in marker values                │
│                                         │
│  Encryption at rest: AES-256 (default)  │
│  Backups: encrypted + geo-redundant     │
└─────────────────────────────────────────┘

Supabase Schema

-- Scan results table
CREATE TABLE scans (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  scan_date TIMESTAMPTZ NOT NULL DEFAULT now(),
  scan_types TEXT[] NOT NULL,
  encrypted_results BYTEA NOT NULL,
  iv BYTEA NOT NULL,
  result_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- RLS: users can only see their own scans
ALTER TABLE scans ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users read own scans" ON scans
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users insert own scans" ON scans
  FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users delete own scans" ON scans
  FOR DELETE USING (auth.uid() = user_id);

-- Audit log (append-only, no delete)
CREATE TABLE scan_access_log (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  action TEXT NOT NULL,
  scan_id UUID REFERENCES scans(id),
  ip_address INET,
  user_agent TEXT,
  accessed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Index for fast user lookups
CREATE INDEX idx_scans_user ON scans(user_id, scan_date DESC);
CREATE INDEX idx_audit_user ON scan_access_log(user_id, accessed_at DESC);
📜
HIPAA Rules — What Each One Means
Plain-English breakdown of every requirement
RuleWhat It MeansHow to ImplementCost
Encryption at Rest PHI must be encrypted when stored on any disk, database, or backup Supabase Pro encrypts at rest by default (AES-256). Add app-level encryption on marker values before storing. $0 (built-in)
Encryption in Transit PHI must be encrypted during transfer between client and server HTTPS only (TLS 1.2+). Never allow HTTP fallback. Supabase enforces HTTPS. $0 (default)
Access Controls Only authorized users can see health data. Minimum necessary access. Supabase RLS + Auth. Each user can only read their own rows. No global admin data view without audit. $0 (built-in)
Audit Trail Log every access to PHI: who, when, what, from where. Keep 6 years. scan_access_log table. Log on every read/write. 6-year retention policy. ~$5/mo storage
BAA Written agreement with every vendor that touches PHI Supabase Pro: signs BAA. Vercel Enterprise: signs BAA. Stripe: has BAA for billing if tied to health services. $25/mo (Supabase Pro)
Minimum Necessary Only collect and store what's needed. Don't over-collect. Store marker values only. No raw video, no camera frames, no audio recordings. Delete raw data immediately after processing. $0 (architecture decision)
Breach Notification Notify affected users within 60 days of discovering a breach. Notify HHS if >500 people affected. Incident response plan document. Email notification system. Breach assessment template. $0 (document)
Data Disposal Delete PHI when no longer needed. Users must be able to delete their data. "Delete my data" button. Cascading delete on user account deletion. Auto-purge policy (e.g., 3 years inactive). $0 (code)
Risk Assessment Annual assessment of risks to PHI. Document threats and mitigations. Annual review document. Can be simple spreadsheet for a startup. Update when architecture changes. $0 (document)
📦
Vendor BAA Status — Your Stack
Which vendors in your stack sign BAAs and at what tier
VendorBAA Available?Plan RequiredMonthly CostNotes
Supabase✓ YesPro ($25/mo)$25+Postgres encrypted at rest, RLS built-in, SOC 2 Type II
Vercel✓ Enterprise onlyEnterprise (custom)$$$For Option A (client-side), Vercel just serves static files — no PHI touches Vercel, so no BAA needed
Cloudflare✓ YesEnterprise$$$CDN/DNS only for Option A — no BAA needed if no PHI in transit headers
Stripe✓ YesStandard (free)$0Only needed if billing is tied to health services
GitHub✗ NoN/AN/ANever push PHI to repos. Code only, no health data.
Option A cost: $0/mo
If you go client-side only, you need zero BAAs. Vercel/Cloudflare serve static HTML. The phone processes everything. Your servers never see health data. Total HIPAA compliance cost: $0.
Option B cost: ~$25/mo
Supabase Pro ($25/mo) with BAA signed. That's your only mandatory cost. Vercel doesn't need a BAA if the API routes that handle PHI go directly to Supabase (not through Vercel serverless functions).
🚫
What NOT to Store — Ever
Data that should be processed and immediately discarded
Data TypeProcess?Store?Why
Raw camera video frames✓ In memory✗ NeverFacial biometric data under BIPA/GDPR. Process → extract markers → discard frames immediately.
Audio recordings (voice/breath)✓ In memory✗ NeverVoice is biometric. Extract features → discard audio.
Tongue/nail photos✓ In memory✗ NeverMedical images. Process → extract color/texture values → discard photos.
Face mesh coordinates✓ In memory✗ NeverBiometric identifier. Use for rPPG ROI extraction only → discard.
Marker values (numbers)✓ EncryptedThis is what you store — just the 346 numeric results. No images, no video, no audio.
Scan metadata (date, types)✓ OK"User did a face scan on April 13" is low-risk metadata.
User profile (age, sex, height)✓ EncryptedNeeded for age/sex-normalized reference ranges.
Critical Rule
The camera feed, audio, and photos must NEVER leave the device or be stored anywhere — not on the server, not in a temp file, not in a log. Process in a Web Worker or OffscreenCanvas, extract the numeric markers, and let the garbage collector destroy the raw data.
Implementation Checklist
Step-by-step for both options

Option A Checklist (Client-Side MVP)

  • Process all scan data in-browser (Web Worker / OffscreenCanvas)
  • Store marker results in IndexedDB with Web Crypto AES-256-GCM encryption
  • Never send health data to any server
  • Add "Export to PDF" for user-initiated sharing
  • Add "Delete All My Data" button that clears IndexedDB
  • Privacy policy stating "your health data never leaves your device"
  • No analytics on health data (analytics only on app usage, not scan results)
  • Discard all camera/audio/photo data after processing (no temp files)

Option B Checklist (Server-Side, add when needed)

  • Sign BAA with Supabase (upgrade to Pro, request BAA via dashboard)
  • Create scans table with encrypted_results (bytea) column
  • Enable RLS on scans table — user can only access own rows
  • Encrypt marker values client-side BEFORE sending to Supabase
  • Create scan_access_log audit table (append-only)
  • Log every read/write to scans with user_id, IP, timestamp
  • Set 6-year retention on audit logs
  • Add "Delete My Account" that cascading deletes all scans
  • Write incident response plan (breach notification within 60 days)
  • Annual risk assessment document
  • Privacy policy updated with data collection, storage, and sharing details
  • Terms of service updated with HIPAA notice
🎯
Bottom line: Launch with Option A (client-side). Your total compliance cost is $0. When you need cloud features, Supabase Pro at $25/mo with a signed BAA covers everything. The key rule: store numbers only, never images or audio, encrypt everything, and let users delete their data.