atlas

multi-site scheduling · resource coordination

launching 2026· first school cohort

A multi-site scheduler for after-school programs. Coordinates tutors, rooms, grade levels, and time blocks across schools in a single grid — replacing the spreadsheets and group texts most operators run on.

next.js 14 · typescript · supabase (postgres + rls) · zustand · tailwind · vercel

Correctness at the data layer. A check_schedule_conflict trigger on the schedules table rejects any insert that would double-book a room or a tutor. A client-side race can't corrupt the schedule — the database won't accept it.

insertscheduleEntrypostgres triggercheck_schedule_conflictroom · tutor · time windowacceptrow committedrejectconstraint violation
fig.01 — insert path · enforcement at the data layer

View-layer abstractions over flat storage. Splits are JSON config on the school, not a new schema column. Panels are derived views over one entries table. Time blocks are derived from entry start/end times through a pure deriveScheduleBlocks function — one source of truth, zero sync drift.

Single Zustand store with optimistic mutations and rollback. No provider nesting. Clean separation across the Next.js server/client boundary.

Demo mode without compromising production data. An adminClient bypass ships a whole-product walkthrough without auth, while real customer data stays locked behind RLS. Stakeholders evaluate every feature; nobody sees anything they shouldn't.

Filter correctness across shared panels. Multi-schedule panels share sharedBlocks from DayView so time alignment holds, then each panel filters the shared blocks down to its grades. Get the filter wrong and deleting a row in Schedule 1 deletes it in Schedule 2. That happened once. The fix was rewriting the filter contract.

Grade column memo invariant. Columns have to hold up when grades are split across panels AND someone adds a duplicate column via the "+" button AND grades are combined (TK/K merge). The gradeBands memo switched from i >= baseGradeCount to an extraColumnIndices Set the day we added sorted insertion — a simpler invariant broke the moment the insertion model changed.

Row sizing broke three times. Fixed height, overflow-hidden, and condensed card mode all have to be present simultaneously or the grid falls apart. Fix lives in decisions.md because it kept regressing.

Two rendering paths, one data model. Single-schedule schools render the grid directly from DayView. Multi-schedule schools render SchedulePanel instances that each render their own grid. Both paths share the same entry data, time block derivation, drag-drop logic, and expanded card hover.

Every piece is simple in isolation. The interactions between them are where the complexity lives.