OMNIA Inclusion Ltd — Security Posture
Data controller / processor: OMNIA Inclusion Ltd (registered in England & Wales, company no. 17228173). Registered office: 169 High Street, Marske-by-the-Sea, Redcar & Cleveland, TS11 7LN, United Kingdom.
Version: 1.4 Last reviewed: 2026-06-01
Tenancy model
- Every domain table has a non-null
school_idcolumn. - 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'sprofiles.school_id. - Admin-scoped writes additionally require
has_role(uid, 'admin'). - Defence-in-depth status (honest): server functions that write to
tenant tables via
supabaseAdmin(e.g.mergeIntoPlan) re-validateschool_idin application code. Server functions that read via the user-scoped client (e.g. analytics, DSAR export) also apply an explicit.eq("school_id", schoolId)filter as a backstop in case of an RLS regression. The same explicit filter is now applied to timetable delete mutations. A handful of pre-existing reads still rely on RLS alone — these are tracked and being migrated.
Authentication
- Supabase Auth, email + password and Google OAuth.
- Leaked-password protection (HIBP) is enabled at the Auth provider — passwords are checked against the Have I Been Pwned database on signup and password change.
- Roles stored in
user_roles(separate fromprofiles) — never on the user record, to avoid privilege-escalation via profile edit. - Available roles:
superadmin,admin,inclusion_lead,senco,teacher,peep_only,read_only. - MFA (TOTP) is enforced for
adminandsuperadminroles. The/adminand/superadminshells gate render onmfa.getAuthenticatorAssuranceLevel().currentLevel === 'aal2'and on the existence of a verified TOTP factor. Users without one are redirected to/mfato enrol; users with one but ataal1are prompted to step up. Other roles can enrol voluntarily. - Beta access flow: the
/betamagic-link endpoint validates the client-suppliedredirectToagainst the request's own origin to prevent open-redirect / session-theft attacks.
Public surfaces
| Surface | Auth | Rate limit |
|---|---|---|
/p/$token (plan share) | Hashed token + 4-digit PIN | Per-IP and per-token via enforceRateLimit (salted with RATE_LIMIT_SALT, backed by public.rate_limits); plus 5 attempts → 15 min lockout per row |
/parent-voice/$token | Hashed token + 4-digit PIN | Per-IP + per-token via enforceRateLimit; plus per-row lockout |
/pupil-voice/$token | Hashed token + 4-digit PIN | Per-IP + per-token via enforceRateLimit; plus per-row lockout |
/api/public/hooks/* | CRON_SECRET bearer token (timing-safe) — never the publishable/anon key | Cron-only |
enforceRateLimit (from @/lib/rate-limit/check.server) is the first
line of every public token handler. The previously-disclosed gap
(per-row only) is closed.
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.
Tamper-evident: RLS denies UPDATE and DELETE on audit_logs and
system_audit_logs for authenticated and anon roles via explicit
USING (false) policies. Only server-side code holding the service-role
key (i.e. supabaseAdmin in a server function) can modify rows — a
compromised user session cannot edit or erase audit history.
Secrets
| Secret | Purpose |
|---|---|
SUPABASE_SERVICE_ROLE_KEY | Admin client (server only) |
SUPABASE_PUBLISHABLE_KEY | Browser client |
CRON_SECRET | Bearer token for /api/public/hooks/* |
MS_GRAPH_CLIENT_ID / _SECRET / _TENANT_ID | SharePoint discovery + parent-voice mailbox |
LOVABLE_API_KEY | AI Gateway (standard-mode LLM traffic) |
BYOK_ENC_KEY | AES-256-GCM master key wrapping per-school BYOK API keys at rest |
RATE_LIMIT_SALT | HMAC salt for IP / token rate-limit buckets in public.rate_limits |
SUPABASE_SERVICE_ROLE_KEY is only imported via
@/integrations/supabase/client.server, which is blocked from client
bundles by Vite import protection. Application secrets (LOVABLE_API_KEY,
MS_GRAPH_*, BYOK_ENC_KEY, RATE_LIMIT_SALT) never reach the browser.
BYOK (Bring Your Own Key) — Connected tier
Schools on the Connected tier may activate BYOK to route AI traffic to their own Anthropic or Azure OpenAI account instead of the Lovable AI Gateway. Implementation:
- Keys are AES-256-GCM encrypted at rest (
src/lib/byok/crypto.server.ts) usingBYOK_ENC_KEY. The plaintext key is never returned to the browser. Onlyprovider,endpoint,last4,active, andverified_atare surfaced. school_ai_credentials.api_key_ciphertexthas column-levelSELECTrevoked fromauthenticatedandanon. OnlysupabaseAdminreads it, and only to decrypt server-side immediately before an upstream call.callLovableAiChatauto-routes to the school's BYOK when aschoolIdis passed andschool_ai_credentialsisactive. PII scrubbing still applies. A BYOK upstream failure falls back to the Lovable Gateway only where the school has opted in to fallback; otherwise the error surfaces to the school admin.- The school chooses the Azure region (e.g.
uaenorth,uksouth). That region may sit outside the UK/EEA — the school is the controller of its provider relationship and is responsible for the residency decision. Documented in the school's signed DPA.
Credential columns are not readable by the browser client. Across
plan_share_links, parent_voice_requests, pupil_voice_requests,
pending_emails, import_connections, and school_export_settings,
column-level SELECT on pin_hash, pin_salt, token_hash,
webhook_secret, api_key, and sent_to_email is REVOKEd from the
authenticated and anon roles. Even a select('*') issued with the
publishable key cannot return those columns. The only readers are
server functions that explicitly use supabaseAdmin (e.g. PIN
verification on the public plan-share / parent-voice / pupil-voice
endpoints, and MIS connector code on the server).
Additionally, pending_emails SELECT/UPDATE and aa_notification_log
SELECT are restricted at the RLS layer to admin / superadmin only,
and the trigger / RLS helper SECURITY DEFINER functions
(handle_new_user, enforce_signup_open, has_role,
has_school_access, is_superadmin, user_school_id) have their
EXECUTE grants tightened — trigger functions are not callable by
clients at all, and the helpers are callable only by authenticated.
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 (standard mode) processes specialist report text in-flight; we do not control upstream provider retention beyond what the AI Gateway exposes.
- BYOK mode moves AI processing onto the school's own provider tenancy. OMNIA cannot enforce retention, residency, or training-opt-out beyond what the school configures with that provider.
Incident playbook
- Suspected token leak → SENCo revokes the share link from the pupil page; the daily sweep removes the row at 365d.
- Suspected RLS bypass → admin runs the Supabase database linter and
reviews
audit_logsfor unexpectedactor_kind=service. - Account takeover → admin disables the
user_rolesrows for that user; Supabase Auth password reset + session revoke from the dashboard. - Data subject request → use Download data pack on the pupil page for access; Erase pupil record for erasure.
Reporting a vulnerability
Please email security@omnia-inclusion.com with a description of the
issue, reproduction steps, and (where relevant) the affected URL or
endpoint. The machine-readable contact is also published at
/.well-known/security.txt.
We commit to:
- Acknowledge receipt within 2 working days.
- Provide a triage assessment within 5 working days.
- Coordinate disclosure on a 90-day window from triage; we will keep you updated and credit you in the post-mortem unless you ask us not to.
Please do not run automated scans against production tenants or attempt to access data that does not belong to you. Good-faith research that follows this policy will not lead to legal action from us.
For misuse reports (spam, harassment, content takedown), email omnia.abuse@omnia-inclusion.com instead.