Architecture & RLS Strategy
Design the database before you write code. Twelve tables, four relationships you must get right, and the RLS rule that protects everything.
This page is the most important in Project 3. Read it twice. The decisions you make here cascade through every module.
The core idea is simple: data belongs to a branch. Every row in every table — students, attendance, exams, marks, fees, circulars — carries the branch_id it belongs to. Row Level Security uses that one column to filter what every user can see.
Get this right and the rest of the project is straightforward. Get it wrong and you will rewrite half the database.
The 12 Core Tables
Drawn out, the schema looks like this:
Every box (except profiles) has a branch_id. That column is the spine of the whole system.
The Single RLS Rule
Every table follows the same rule: a user can see a row iff their branch_id matches the row's branch_id, or they are a super admin.
In SQL, the basic pattern (repeated per table with minor variations):
This is the spine. Once it's in place for every table, most of your RLS is done.
Per-persona refinements (a parent should only see their own child, not every student in the branch) add a second layer on specific tables. We cover those on the personas page.
Helper Functions
To avoid repeating (select role from public.profiles where id = auth.uid()) everywhere, create two reusable Postgres functions. Run this in the Supabase SQL editor:
Now policies become much shorter:
The Order to Create Tables (Migration Order)
Because of foreign keys, you must create tables in this order. Use one migration per major group.
branches(no dependencies)profiles(referencesbranchesviabranch_id, but the column is nullable — super admins have no branch)class_sections(referencesbranches)teachers(referencesprofiles,branches)- Now update
class_sections.teacher_idforeign key to point atteachers.id parents(referencesprofiles)students(referencesparents,branches,class_sections)attendance(referencesstudents)exams(referencesbranches)exam_marks(referencesstudents,exams)fee_plans(referencesbranches)fee_payments(referencesstudents,fee_plans)circulars(referencesbranches)
If you try to create students before parents, Supabase will error. The migration scripts go in supabase/migrations/ in numbered files (00001_branches.sql, 00002_profiles.sql, etc.).
Database Conventions
To keep the schema readable across 12 tables, every table follows the same conventions:
- Primary key:
id uuid primary key default gen_random_uuid() - Created/updated:
created_at timestamptz not null default now()+ a trigger to maintainupdated_at - Soft delete:
deleted_at timestamptz(null = active; never hard-delete) - Branch column:
branch_id uuid not null references public.branches(id) on delete restrict(exceptprofileswhere it's nullable) - RLS enabled: every single table — no exceptions
You will write a small migration that adds the updated_at trigger function once, then reuse it on every table.
What This Schema Does NOT Have (Yet)
A few things you'll probably want eventually, but they're not in the core 12:
- Subjects — for now exams have a
subjecttext column. Later, normalise to asubjectstable. - Academic year — for now exams and fees use a
datecolumn. For a real multi-year system, add anacademic_yeartable. - Document uploads — student photos, exam papers. Use Supabase Storage; reference the file path in a column. We'll touch this in the Students module.
- Audit log — every important action should be logged for the super admin to review. Add
school_audit_loglater.
Don't add these to your initial migration. They'll only slow you down. Add them when you actually need them.
The Permission Matrix (Preview)
You'll work out the full permission matrix on the next page (personas.mdx). But here's the executive summary:
| Action | Super | Branch admin | Teacher | Parent | Student |
|---|---|---|---|---|---|
| Create / edit branches | ✓ | — | — | — | — |
| Create / edit student | ✓ | ✓ (own branch) | — | — | — |
| Mark attendance | ✓ | ✓ | ✓ (own sections) | — | — |
| Enter exam marks | ✓ | ✓ | ✓ (own sections) | — | — |
| View own child's data | n/a | n/a | n/a | ✓ | — |
| View own data | n/a | n/a | n/a | n/a | ✓ |
| Pay fees | ✓ | ✓ | — | ✓ | — |
| Send circulars | ✓ | ✓ (own branch) | — | — | — |
| Receive circulars | — | ✓ | ✓ | ✓ | ✓ |
This matrix is the source-of-truth for every RLS policy you'll write.
The Three Things to Get Right Before Writing Any Code
- The 12-table schema is migrated. You can
select * from studentsand see an empty table. is_super_admin()andcurrent_branch_id()work. Test them in the SQL editor while logged in as different users.- The basic "branch_or_super" RLS policy is on every table. Test by signing in as a branch admin and querying — you should see only their branch.
When all three are true, the rest of Project 3 is "build features" — not "fight RLS."
The next page is personas.mdx — the full permission matrix and how each one logs in, what they see, and what they can do.