Supabase RLS for HIPAA-Safe AI Access Patterns
Healthcare apps using AI need to ensure PHI only flows where it should. Supabase Row Level Security is the cheapest enforcement layer. Here's the actual policy pattern.
If you're building a healthcare app on Supabase that uses AI, the PHI access patterns must be enforced at the database, not just at the application code.
This is the RLS pattern I use. Not legal advice. Talk to your privacy counsel.
The threat model
The risk is unintended PHI exposure. Specifically: - AI service receiving more PHI than necessary - Cross-patient data leakage if an AI session has stale context - Staff viewing PHI for patients they don't have a treatment relationship with - Auditors unable to prove "minimum necessary" access was enforced
The application layer can enforce these. But application-layer enforcement is one bug away from a breach. Database-layer enforcement is more durable.
The RLS pattern
Step 1: Patient assignment table. Every patient is assigned to one or more providers. The assignment is the basis for access.
```sql create table patient_provider_assignments ( id uuid primary key default gen_random_uuid(), patient_id uuid references patients(id) not null, provider_id uuid references auth.users(id) not null, assigned_at timestamptz default now(), unassigned_at timestamptz, reason text ); ```
Step 2: A helper function for "current user can access this patient".
```sql create or replace function can_access_patient(patient_uuid uuid) returns boolean language plpgsql security definer as $$ begin return exists ( select 1 from patient_provider_assignments where patient_id = patient_uuid and provider_id = auth.uid() and unassigned_at is null ); end; $$; ```
Step 3: RLS on every PHI-containing table.
```sql alter table patients enable row level security; alter table clinical_notes enable row level security; alter table prescriptions enable row level security;
create policy patients_select on patients for select using (can_access_patient(id));
create policy clinical_notes_select on clinical_notes for select using (can_access_patient(patient_id));
create policy prescriptions_select on prescriptions for select using (can_access_patient(patient_id)); ```
A provider can only see patients they're currently assigned to. Period.
The AI access pattern
The application code calling AI services queries Supabase using the requesting user's auth context. The RLS policies enforce that the data returned to the application is filtered to what that user can see.
The AI service then receives only what the application can read. Three layers of constraint: 1. RLS prevents pulling data the user shouldn't see 2. Application code limits what's sent to AI 3. AI vendor has BAA + no-training terms
If any one layer fails, the others catch.
A specific AI access pattern: clinical summary generator
A provider wants an AI-generated summary of a patient's recent visits.
```typescript const supabase = createServerClient({ /* user auth context */ });
// RLS will reject if provider isn't assigned to this patient const { data: notes, error } = await supabase .from("clinical_notes") .select("note_date, note_type, content") .eq("patient_id", patientId) .gte("note_date", sixMonthsAgo) .order("note_date", { ascending: false });
if (error) throw new Error("not_authorized");
// AI service: only sees notes RLS allowed through const summary = await aiService.summarize({ notes, patient_initials: data.initials, // not full name request_id: requestId, // for audit logging }); ```
If the provider isn't assigned, RLS returns nothing. The AI service gets nothing. No PHI exposure path.
The audit-trail pattern
Pair RLS with an audit log:
```sql create table phi_access_log ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users(id) not null, patient_id uuid references patients(id) not null, access_type text not null, -- "view" | "ai_summary" | "export" | etc. ai_vendor text, -- if applicable request_id text, -- correlate with vendor logs accessed_at timestamptz default now() ); ```
Every PHI access path logs to this table. Auditors can query: "show me every time PHI for patient X was accessed and who accessed it."
The minimum-necessary pattern
HIPAA requires "minimum necessary" PHI access. RLS supports this with column-level restrictions.
```sql -- A nurse can see notes content but not billing info create policy clinical_notes_nurse_select on clinical_notes for select to nurse_role using (can_access_patient(patient_id));
-- A billing clerk can see CPT codes and amounts but not note content create view billing_view as select id, patient_id, visit_date, cpt_codes, amount from clinical_notes where can_access_patient(patient_id);
grant select on billing_view to billing_role; ```
Different roles get different slices of PHI. Enforced at the database.
What broke
RLS performance on large datasets. The `can_access_patient` function gets called on every row. For a patient with thousands of clinical notes, this became a bottleneck. We added an index on `patient_provider_assignments(provider_id, patient_id) where unassigned_at is null`. Query time dropped from 800ms to 25ms.
Service-role bypass. Server-side code that uses Supabase's service role bypasses RLS. This is intentional for migrations and admin operations. We added an audit log entry for every service-role query touching PHI, and a code review rule that service-role queries on PHI tables require justification.
Cross-database queries. If you have a separate analytics database that gets PHI mirrored over, you need to enforce there too. RLS only protects Supabase. We added separate access controls in our analytics tier.
What this isn't
RLS is not a complete HIPAA program. You still need: - BAA with Supabase (which Supabase offers on their enterprise tier) - BAA with AI vendors that touch PHI - Audit logging (separate from RLS) - Staff training - Breach response plan - Risk assessment - Documented policies
RLS is the technical enforcement layer. The compliance program is the wrapper around it.
What to build first
If you're starting a HIPAA-touching app on Supabase:
One, design the access model up front. Who sees what patient under what conditions? This is the hardest part. Get it right before writing code.
Two, RLS on every PHI table. No table without RLS. No exceptions.
Three, audit log for every PHI access. The audit log is the proof you'll need at audit time.
Four, BAAs with Supabase and any AI vendors. The technical work doesn't matter if the legal layer isn't in place.
Not legal advice. Talk to your privacy counsel before deploying healthcare apps.
Want the full guide? Check out our deep-dive page for more context, FAQs, and resources.
read the full guide