RowShield
Guides

Student data on Supabase: scoping enrolments, marks and messages

Education platforms hold an awkward truth: children’s data attracts more curiosity than adult data, from classmates to strangers. A class list, a mark sheet, a guardianship record — each is mundane inside a school and sensitive outside it. On Supabase the protection is structural: enrolment-scoped RLS that makes one class unable to reach another’s rows.

The domain helps here. Schools already think in relationships — this teacher teaches this class, this guardian belongs to this pupil — and those relationships translate directly into policy traversals. The work is mostly honesty about edges: supply teachers, siblings sharing a guardian, pupils moving classes mid-term, and the exports and dashboards that quietly widen access.

Note the same boundary as elsewhere in sensitive domains: this page covers technical isolation only. Safeguarding and data-protection duties for children vary by jurisdiction and sit well above database configuration; treat what follows as the floor beneath those duties, never the ceiling.

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.

Scope by class, not by school

The tempting shortcut is a single policy keyed on school id: everyone in the school sees everything. That fails the moment one class’s marks concern only its teacher. Model the teaching relationship — a class_teachers table linking staff to classes — and derive visibility by traversal, so a physics teacher has no path to a drama class’s assessment rows.

Guardians need their own narrow lane. Link guardians to pupils explicitly, then let guardians read only enrolment and progress rows for their own children. The join indirection feels heavier than a flat column at design time and repays itself the first time a family structure is anything other than two parents and one child.

ALTER TABLE public.enrolments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.enrolments FORCE ROW LEVEL SECURITY;

CREATE POLICY "class_teachers_read_enrolments"
  ON public.enrolments
  FOR SELECT
  TO authenticated
  USING (class_id IN (
    SELECT ct.class_id FROM public.class_teachers ct
    WHERE ct.teacher_id = (SELECT auth.uid())
  ));

CREATE POLICY "guardians_read_own_children"
  ON public.enrolments
  FOR SELECT
  TO authenticated
  USING (pupil_id IN (
    SELECT gl.pupil_id FROM public.guardian_links gl
    WHERE gl.guardian_user_id = (SELECT auth.uid())
  ));

Marks, drafts and the export path

Assessment data has a lifecycle: entered, moderated, published. A draft mark visible to pupils spoils the pedagogy and the weekend; gate publication behind a status column so pupil-facing policies filter on it while staff policies do not. Two policies per audience beat one clever condition, because readers can tell at a glance which grant they fall under.

Exports deserve their own paragraph of suspicion. CSV downloads and reporting jobs frequently run with elevated credentials and reproduce entire tables into files whose subsequent life nobody tracks. Prefer scheduled, server-side exports under dedicated roles, and record what was extracted, when, and for whom — the discipline costs minutes and answers the question everyone asks eventually.

Test like a supply teacher

The best test borrows the domain’s own chaos. Create a supply teacher account attached to one class and attempt to read another class’s register — expect emptiness. Sign in as a guardian of two pupils and confirm visibility matches the links and nothing wider. These scenarios encode the platform’s promises as demonstrated behaviour rather than intention.

Repeat the exercises each term, not each decade. Timetabling churn, new intakes and mid-year moves mutate the relationship tables constantly, and a policy that traverses relationships is only as current as they are. A quarterly half-hour of role-based probing keeps the guarantee aligned with the timetable it depends on.

Continuous watching between terms

Between termly reviews, configuration still changes: an integration ships a staging table, a debug script disables a policy overnight, a new bucket collects homework scans. Someone or something must notice, and manual notice scales badly across an academic calendar that concentrates change into frantic windows. Automation suits this rhythm precisely because it ignores calendars.

RowShield provides that automation for configuration states: scheduled scans snapshot every table’s RLS posture, diffs drive alerts on transitions, and history distinguishes new findings from regressions. It reads catalogue and bucket metadata only — never pupil rows — and is an independent product unaffiliated with Supabase. The safeguarding judgments remain, rightly, with your team.

Frequently asked

Can pupils see classmates’ work through the API?
Only if a policy permits it. With enrolment-scoped RLS, a pupil’s queries traverse their own links and stop there, so another pupil’s rows simply do not appear regardless of how the request is crafted. Verify once with two test accounts and you have converted hope into evidence.
How should siblings under one guardian behave?
Naturally, if guardianship is a link table: the guardian’s policy traverses links, finds both pupils, and sees exactly those rows. The structure also handles messier families without special cases — additional carers gain rows in the link table rather than edits scattered through policy logic.
Do we still need the school’s own access review?
Yes. Database policies implement decisions; they do not make them. Who counts as staff, how long leavers retain access and which partners receive extracts are institutional judgments belonging to the school or platform owner, reviewed on a schedule the institution sets.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase student data protectioneducation platform rlsschool records postgres isolationguardian access supabase policies