Enrolment form, list view, search, and profile pages. Every other module references this one.
The Students module is your first real feature. By the end of this page you can:
Open /admin/students and see a paginated table of all students
Filter by branch, grade, section, or search by name
Open a student's profile
Enrol a new student via a form
Edit existing student details
Soft-delete (and restore) a student
You'll use TanStack Query for server state, React Hook Form + Zod for the form, and Supabase as the data source. RLS in the database does the heavy lifting — branch admins see only their branch automatically.
The migration (referenced in the architecture page) — full version:
create table public.students ( id uuid primary key default gen_random_uuid(), admission_no text not null, full_name text not null, date_of_birth date not null, gender text check (gender in ('male', 'female', 'other')), parent_id uuid not null references public.parents(id) on delete restrict, branch_id uuid not null references public.branches(id) on delete restrict, section_id uuid references public.class_sections(id), roll_no text, photo_url text, emergency_phone text, notes text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz, unique (branch_id, admission_no), unique (section_id, roll_no));alter table public.students enable row level security;-- Branch isolation: super admin sees all, branch admin sees own branch,-- teacher sees students in their assigned sections, parent sees own children.create policy "students_select" on public.students for select using ( public.is_super_admin() or branch_id = public.current_branch_id() or ( -- Parents: only their own children parent_id in (select id from public.parents where id = auth.uid()) ) or ( -- Teachers: only students in their sections section_id in ( select id from public.class_sections where teacher_id = auth.uid() ) ) );create policy "students_insert" on public.students for insert with check ( public.is_super_admin() or branch_id = public.current_branch_id() );create policy "students_update" on public.students for update using ( public.is_super_admin() or branch_id = public.current_branch_id() );create policy "students_delete" on public.students for delete using ( public.is_super_admin() );
A few things worth noticing:
unique (branch_id, admission_no) — admission numbers are unique within a branch, not globally.
unique (section_id, roll_no) — two students can't have the same roll in the same section.
Separate select / insert / update / delete policies — different rules per operation. Only super admins can hard-delete (we soft-delete by default).
auth.uid() for parents and teachers — the policy reads who the current user is and filters appropriately. No UI guard required.
Run this in Supabase SQL editor or supabase/migrations/.
pnpm supabase gen types typescript --linked > src/lib/database.types.ts
Now you have Database['public']['Tables']['students']['Row'] available everywhere. Don't write your own Student interface — derive it:
import type { Database } from '@/lib/database.types'export type Student = Database['public']['Tables']['students']['Row']export type StudentIn = Database['public']['Tables']['students']['Insert']
This means when the schema changes, types update automatically. You will run the gen types command after every schema change. Add it to your mental routine.
That's it. The same <StudentsTable> works for super admin (sees everyone) AND branch admin (sees own branch) AND teacher (sees own section) — because RLS does the filtering.
You don't write any "if branch admin then..." in the UI. The database decides.
You have the foundation: real students in the database, properly isolated. The next module — Attendance — is much smaller because it builds on top of this one.