Back home
# OMNIA — Security Posture **Version:** 1.0 **Last reviewed:** 2026-05-10 ## Tenancy model - Every domain table has a non-null `school_id` column. - Row-Level Security is enabled on every domain table. - Read / write policies use the `has_school_access(uid, school_id)` SECURITY DEFINER function, which checks the caller's `profiles.school_id`. - Admin-scoped writes additionally require `has_role(uid, 'admin')`. - All `supabaseAdmin` (service-role) writes that target tenant tables re-validate `school_id` in application code as a defence-in-depth layer (see `mergeIntoPlan` in `src/lib/parent-voice/public.functions.ts`). ## Authentication - Supabase Auth, email + password and Google OAuth. - Roles stored in `user_roles` (separate from `profiles`) — never on the user record, to avoid privilege-escalation via profile edit. - Available roles: `superadmin`, `admin`, `inclusion_lead`, `senco`, `teacher`, `peep_only`, `read_only`. ## Public surfaces | Surface | Auth | Rate limit | |---|---|---| | `/p/$token` (plan share) | Hashed token + 4-digit PIN | 5 attempts → 15 min lockout | | `/parent-voice/$token` | Hashed token + 4-digit PIN | 5 attempts → 15 min lockout | | `/api/public/hooks/retention-sweep` | `apikey` header (publishable key) | Cron-only, 1×/day | Tokens are stored as SHA-256 hashes; PINs are scrypt-hashed with a per-row salt. The raw token / PIN is shown to the issuing staff member exactly once. ## Audit log `audit_logs` captures every read/export/share event. Retention 730 days. SENCo and admin can read their own school's log. Per-pupil panel surfaces the last 90 days on the pupil page for inspector handover. ## Secrets | Secret | Purpose | |---|---| | `SUPABASE_SERVICE_ROLE_KEY` | Admin client (server only) | | `SUPABASE_PUBLISHABLE_KEY` | Browser client + cron auth | | `MS_GRAPH_CLIENT_ID` / `_SECRET` / `_TENANT_ID` | SharePoint discovery + parent-voice mailbox | | `LOVABLE_API_KEY` | AI Gateway (specialist-summary LLM) | No secret is exposed to the browser. `SUPABASE_SERVICE_ROLE_KEY` is only imported via `@/integrations/supabase/client.server`, which is blocked from client bundles by Vite import protection. ## Known limitations (disclosed) - **Scanned PDFs** uploaded to Specialist Summary are not OCR'd; image-only PDFs return empty text and the staff member is asked to re-key. - **SharePoint discovery** matches on national ID — pupils without an ID configured are skipped (counted in the run report). - **AI Gateway** processes specialist report text in-flight; we do not control upstream provider retention beyond what the AI Gateway exposes. ## Incident playbook 1. **Suspected token leak** → SENCo revokes the share link from the pupil page; the daily sweep removes the row at 365d. 2. **Suspected RLS bypass** → admin runs the Supabase database linter and reviews `audit_logs` for unexpected `actor_kind=service`. 3. **Account takeover** → admin disables the `user_roles` rows for that user; Supabase Auth password reset + session revoke from the dashboard. 4. **Data subject request** → use **Download data pack** on the pupil page for access; **Erase pupil record** for erasure.