All tables use business unit (idbu) filtering to isolate data:
-- Example from vital signs catalog-- From: 20251011221451_create_vital_signs_catalog_table.sql:90-101CREATE POLICY "Users can view vital signs from their BU" ON "tcSignosVitales" FOR SELECT TO authenticated USING ( idbu = ( SELECT idbu FROM "tcUsuarios" WHERE idusuario = auth.uid() ) );
How it works:
User authenticates with Supabase Auth
auth.uid() returns the authenticated user’s UUID
Policy looks up the user’s business unit from tcUsuarios
Only rows matching that business unit are accessible
This architecture ensures complete data isolation between different medical practices using the same database.
CREATE POLICY "Users can view records from their BU" ON "tableName" FOR SELECT TO authenticated USING ( idbu = (SELECT idbu FROM "tcUsuarios" WHERE idusuario = auth.uid()) );
RLS policies that query the same table can cause infinite recursion. DoctorSoft+ uses security definer functions to avoid this:
-- From: 20251003185652_fix_get_user_idbu_simple_function.sqlCREATE OR REPLACE FUNCTION get_user_idbu_simple()RETURNS uuidLANGUAGE sqlSECURITY DEFINERSET search_path = publicSTABLEAS $$ SELECT idbu FROM "tcUsuarios" WHERE idusuario = auth.uid() LIMIT 1;$$;-- Use in policiesCREATE POLICY "Access records in same BU" ON "someTable" FOR SELECT TO authenticated USING (idbu = get_user_idbu_simple());
Why this works:
SECURITY DEFINER runs with elevated privileges, bypassing RLS
STABLE marks the function as cacheable within a transaction
Returns only the authenticated user’s business unit
-- From: 20251002182941_create_appointment_statuses_table.sql:83-87CREATE POLICY "Authenticated users can read appointment statuses" ON "tcCitasEstados" FOR SELECT TO authenticated USING (true);
These tables:
Are read by all authenticated users
Have no INSERT/UPDATE/DELETE policies for regular users
-- From: 20250517220216_steep_sun.sql:24-35CREATE POLICY "Access patients in same business unit" ON "tcPacientes" FOR ALL TO authenticated USING ( idbu = ( SELECT idbu FROM "tcUsuarios" WHERE idusuario = auth.uid() ) AND deleted_at IS NULL -- Exclude soft-deleted records );
Additional protections:
Soft deletion instead of hard deletion (deleted_at column)
User ID tracking for audit trails (user_id column)
-- From: 20251003181437_create_activity_tracking_table_v2.sqlCREATE POLICY "Usuarios ven actividades de su BU" ON "tcActividadReciente" FOR SELECT TO authenticated USING (idbu = get_user_idbu_simple());CREATE POLICY "Usuarios autenticados insertan actividades" ON "tcActividadReciente" FOR INSERT TO authenticated WITH CHECK (id_usuario = auth.uid());CREATE POLICY "Actividades son inmutables" ON "tcActividadReciente" FOR UPDATE TO authenticated USING (false); -- No updates allowedCREATE POLICY "Solo admins eliminan actividades" ON "tcActividadReciente" FOR DELETE TO authenticated USING ( EXISTS ( SELECT 1 FROM "tcUsuarios" WHERE idusuario = auth.uid() AND rol = 'admin' ) );
This ensures:
Complete audit trail of all user actions
Activity records cannot be modified after creation
Only administrators can delete activity logs
Each activity is tied to a specific user and business unit
-- Allow authenticated users to upload filesCREATE POLICY "Authenticated users can upload files" ON storage.objects FOR INSERT TO authenticated WITH CHECK ( bucket_id = '00000000-default-bucket' AND (storage.foldername(name))[1] = auth.uid()::text );-- Allow users to read files from their business unitCREATE POLICY "Users can read files from their BU" ON storage.objects FOR SELECT TO authenticated USING ( bucket_id = '00000000-default-bucket' AND (storage.foldername(name))[1] IN ( SELECT idbu::text FROM "tcUsuarios" WHERE idusuario = auth.uid() ) );-- Allow users to delete their own filesCREATE POLICY "Users can delete their own files" ON storage.objects FOR DELETE TO authenticated USING ( bucket_id = '00000000-default-bucket' AND owner = auth.uid() );
// ✅ Good: Use the anon keyconst supabase = createClient( process.env.VITE_SUPABASE_URL, process.env.VITE_SUPABASE_ANON_KEY);// ❌ Bad: Never use service role key in browserconst supabase = createClient( url, process.env.SUPABASE_SERVICE_ROLE_KEY // DANGEROUS!);
-- Delete activity logs older than 90 daysCREATE OR REPLACE FUNCTION cleanup_old_activity_logs()RETURNS void AS $$BEGIN DELETE FROM "tcActividadReciente" WHERE created_at < NOW() - INTERVAL '90 days';END;$$ LANGUAGE plpgsql SECURITY DEFINER;
Schedule this function to run periodically based on your data retention policy.