Skip to content

Academic tracking for students and teachers

EduTrack

Team of three, which I led · 2026 · React, TypeScript, Supabase

In short

EduTrack gives a student one place for the whole academic picture: grades by module, absences, tasks and deadlines, study sessions, a reflection journal, and analytics across all of it. Teachers follow the students in their groups and take attendance.

I led a team of three and built most of it — the database schema and its migrations, the data layer and the interface. It's a React and TypeScript single-page app on Supabase, which means PostgreSQL does more of the work than it would behind a typical front end.

The problem

Students track their studies across a spreadsheet, a notes app and memory. The numbers that matter — an average slipping, absences adding up — are scattered until it's late to act on them. EduTrack puts them in one place, updated as grades and absences are recorded.

There's a second audience, and it shapes the data model: teachers need to follow a group without seeing students outside it, while some things — a student's reflections, their study sessions — are nobody's business but the student's. So who can see what is part of the product, not an afterthought.

Architecture

  • Supabase provides PostgreSQL, authentication and the API. The schema lives in versioned SQL migrations, and a database trigger creates each user's profile the moment they sign up.
  • Row-level security is enabled on every table. A student's tasks, grades and absences are shared with the teachers of that student's groups; reflections and study sessions stay private to the student.
  • Groups sit between teachers and students, and a single database function answers "does this teacher teach this student?" for every policy that needs to know.
  • The client is React with TypeScript and React Router. Data access lives in hooks and services rather than inside components, and Recharts draws the analytics.

Decisions & trade-offs

The database creates the profile, not the client

A user without a profile row is a bug waiting in every query that joins them. Creating the profile in a trigger on signup — in the same transaction as the account — makes that state impossible, rather than something the client has to remember to prevent.

Groups, not a teacher field on every student

A teacher teaches several groups; a student can belong to more than one. Modelling that as groups with two membership tables, and asking "does this teacher teach this student?" through one shared function, means every access policy asks the same question the same way — one place to reason about, one place to change.

What was hard

Row-level security is unforgiving in both directions. A policy that's too loose leaks data silently; one that's too strict returns an empty list, which looks exactly like having no data. Adding teachers and groups meant replacing the original own-data policies wholesale in a second migration and deciding, table by table, what a teacher should reach and what should stay private.

The rest was coordination: three people on one schema, where a migration one person writes changes what everyone else's queries return.

Outcome

Deployed and usable end to end: sign up as a student or a teacher, join a group, and track a semester — grades on a 0–20 scale by module and type, attendance taken by the teacher for the whole group, tasks with subtasks, and the analytics on top.

What I'd do next

Test the access policies directly. Each one is a small program, and at the moment the only proof it behaves is using the app.

Then build the two features the landing page promises and the app doesn't have yet: a projected semester average, and a warning before a student reaches a module's absence limit.