Health data isolation on Supabase: configuration you can defend
Health applications carry a duty general software does not: records about a person’s body and care must move only along clinical relationships. On Supabase that duty translates into concrete configuration — every clinical table fenced by RLS, every policy scoped to the practitioner or patient holding the relationship, every access path deliberate rather than inherited from a tutorial.
The stakes justify the care. An appointments table leaking is not an embarrassment; it is a disclosure of who attends which clinic and when. Isolation therefore has to hold against the least sophisticated attacker available — someone with the public anon key and curiosity — because that attacker exists for every public URL whether or not the app advertises itself.
A boundary note before the detail: this page addresses technical isolation only. Legal duties around health data vary by jurisdiction, and no configuration checklist, scan result or tool substitutes for qualified advice on the obligations that apply to your product.
RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.
Model the clinical relationship first
Isolation policies are only as sound as the relationship they encode, so start with the data model. Who treats whom is usually a table: practitioners, patients, and appointments linking them. Once that linkage exists, every policy becomes a traversal — a practitioner sees the appointments they hold; a patient sees their own record — and authorisation stops being vibes.
Resist the shortcut of a single boolean such as is_staff. Staff roles fragment in practice — locums, receptionists, auditors — and a coarse flag outlives its accuracy. Distinct roles in policies cost little at design time and keep the access story legible when an auditor, a successor developer or a dispute asks exactly who could see what.
ALTER TABLE public.appointments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.appointments FORCE ROW LEVEL SECURITY;
CREATE POLICY "practitioner_reads_own_appointments"
ON public.appointments
FOR SELECT
TO authenticated
USING (practitioner_id = (SELECT auth.uid()));
CREATE POLICY "patient_reads_own_appointments"
ON public.appointments
FOR SELECT
TO authenticated
USING (patient_user_id = (SELECT auth.uid()));
CREATE POLICY "clinic_inserts_scheduled_appointments"
ON public.appointments
FOR INSERT
TO authenticated
WITH CHECK (practitioner_id = (SELECT auth.uid())
OR patient_user_id = (SELECT auth.uid()));Fence the quiet paths
Clinical schemas grow side doors: a notes table joined during development, an export staging table, a reporting view built wide for convenience. Each needs the same scrutiny as the headline tables, because PostgREST serves views as readily as tables. Inventory everything reachable and retire or fence anything whose purpose nobody can state in a sentence.
Storage deserves particular suspicion in health products. Referral letters and scanned forms land in buckets far more often than schemas admit, and a public bucket bypasses every careful table policy at a stroke. Default buckets to private, issue signed URLs with short lifetimes for document access, and log which code paths issue them.
Verify isolation as behaviour, not hope
Configuration reads as intent; tests demonstrate behaviour. After writing policies, sign in as a patient account and attempt to read another patient’s appointments — expect an empty result. Sign in as a practitioner outside the relationship and repeat. These five-minute experiments catch more real defects than any amount of re-reading policy text, because they exercise the exact path a browser takes.
Document the outcome. A dated artifact recording which roles were tested against which tables, with results, is what lets a future colleague distinguish verified isolation from assumed isolation. Rebuild the artifact whenever the schema changes meaningfully, because relationships drift as features accrete.
Ongoing assurance
Drift is the enemy in long-lived clinical systems: a contractor adds a table, a hotfix disables a policy during debugging and restores it incorrectly, a new bucket opens for a partner integration. Periodic re-verification catches these, but manual schedules slip precisely when the team is busy — which correlates strongly with when changes happen.
RowShield does not automate this review, and it issues no certifications; what it offers is continuous configuration watching. Scheduled scans compare every table’s RLS state and policy surface against the last known snapshot and alert on transitions. It reads catalogue and bucket metadata only — never clinical rows — and is an independent product unaffiliated with Supabase.
Frequently asked
- Does passing these checks make our app compliant?
- No. Compliance obligations are legal questions depending on jurisdiction, role and data flows; configuration hygiene is one input among many. Treat the isolation work here as the technical floor beneath whatever formal process your situation requires, and take the formal questions to qualified advisers.
- Should practitioners and patients share one access condition?
- They share the data, not the grant. Separate policies per role on the same table express the two relationships cleanly — each names its role and its ownership column — whereas one blended condition tends to widen quietly as edge cases arrive.
- Where do analytics fit?
- Prefer aggregate pipelines running server-side under controlled credentials, exposing computed results to dashboards. Granting client roles broad read on raw clinical tables to power charts trades a durable isolation guarantee for convenience, and the trade worsens every quarter the tables grow.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit