Jeff Isaak More stories

Chapter 3

The first thirty-three hours

Deciding what to do differently, and building the first slices of a student information system.

Covers November 5 to 7, 2026. Very part-time.

The first commit went in at 8:18 on a Thursday evening in November. Thirty-three and a half hours and twenty-three commits later, there was a working system with students, families, enrolment history and a school year’s timetable structure, running against about 2,500 fictional students in two fictional school districts.

That sounds faster than it was. This is a part-time project, and very part-time at that: the commits are stamped with evenings and early mornings, around a full-time job. Most of the speed comes from the tools I described in Chapter 0, and from a lot of deciding before any building.

Ground rules

A few rules were set on day one, and written down where both I and the model have to read them:

  • No real student data, ever. Not in the code, the tests or the demos. Everything is generated.
  • No third-party services in the data path. No analytics, no hosted error tracking, no LLM calls on student data. It runs entirely from a container on a district’s own server.
  • Accessible from the start. WCAG 2.2 AA, checked automatically on every page the tests visit.
  • No words written into screens. Every piece of text comes from a translation file, so French is a matter of translating, not rebuilding.
  • Nothing about BC in the core. More on that below.

Doing it differently

Chapter 2 was, in a sense, the design document. Most of the decisions below exist to make one of its problems impossible, rather than to make it easier to work around.

Every change is a fact

Students, families, households and enrolments aren’t stored as rows that get edited. They’re stored as a log of events: registered, enrolled, moved house, restriction recorded. Each event says who did it and when, and is never changed or deleted. The tables the screens read are built from the log, and only from the log: the database itself refuses any other write to them.

That answers the “stored twice, then repaired” problem directly. There’s one source of truth, and everything else is derived from it, so there’s nothing to synchronise. If a derived table is wrong, you fix the code that builds it and replay the log; for the whole demo district, that’s about 41,500 events in a little over two seconds. It also means questions like “who was enrolled on September 29?”, which the Ministry asks every year, have an exact answer.

No rollover

A student’s grade isn’t worked out from their year of graduation. Enrolments can span school years, and grade placements are dated: Grade 8 from this date, Grade 9 from that one. A student who repeats a year gets a new placement, not a new graduation year. When the school year ends, nothing needs to happen. There’s no rollover job, so there’s nothing for one student’s record to block.

A student's overview: who can pick him up, who to call and in what order, who he must not be released to, his details, his grades at this school as a path from 8 to 12, and his home and siblings.
A student’s overview. The front desk’s questions first: who can pick him up, who to call, and who he must not be released to. All data is fictional.

Districts kept apart by the database

Every table that belongs to a district is protected by the database’s own row-level security. Every query runs with the caller’s district set for that transaction only, and with no district set, nothing comes back at all. A test fails the build if anyone adds a table without a policy. The one deliberate way across, for finding a student who’s moving from another district, returns a small card and nothing more, and every search is logged.

That’s the start of an answer to “phone the other school”: the other school can be found, and asked, inside the system.

BC as a plug-in

British Columbia’s grades, codes, student number, entry and exit reasons and time zone live in a separate package, a “jurisdiction pack”. The core never mentions BC: a test fails the build if it names one of BC’s codes. BC comes first, but it shouldn’t be the only province this could ever work for.

A student's family and contacts, grouped by household: his mother and sibling at one address, his father at another, each with their roles, and a detail panel for the selected contact.
Family and contacts, grouped by home. Two households, each person’s role spelled out in words rather than a priority number. All data is fictional.

What’s built so far

The work is done in slices, each one complete enough to use before the next starts.

Slice 0: the skeleton

Signing in, two fictional districts, and the separation between them, tested in SQL, through the API and in the browser.

Slice 1: people and enrolment

Students, families, households and custody restrictions, all in the event log. Search that forgives typos. The student profile, chosen from seven mock-ups. Registering a student with a check for duplicates. Contacts in a real order, with labelled phone numbers. Restrictions that office staff can record but only administrators can lift.

A student's History tab: a timeline of events by year, such as starting Grade 8 at a secondary school, finishing Grade 7, starting Kindergarten, being registered, and a parent joining a household.
History, read straight from the event log. Every change, who made it, and when. All data is fictional.

Slice 1b: corrections from the manual

Reading MyEducation BC’s own guides turned up things the first slice had wrong, so the second pass fixed them: enrolments that span school years, dated grade placements, planned and cancelled enrolments, and the Standards Manual’s 19 entry codes and 22 exit codes. Changing a student’s legal identity now needs a document. And the student number’s check digit is actually checked.

Slice 2a: the shape of a school year

Timetables for each school and year, with a draft for next year alongside the one in use. Terms, reporting periods, rotating cycle days, bell schedules, rooms, courses and class sections. Every edit is checked for teacher and room clashes, room size and suitability, and overriding a check needs both a permission and a reason.

The year at a glance for a secondary school: two base terms from September to June, days off as dots, full-year and semester terms as bars, and four reporting periods.
The year at a glance. Base terms, days off, terms and reporting periods on one timeline. Drag a boundary to move it. All data is fictional.

Then a long evening and an early morning on making the year usable rather than just correct: a timeline you can drag, a calendar of every school day in its schedule’s colour, a day view where you can drag periods around, splitting a term so every class keeps meeting in both halves, overriding the rotation for a single day, and copying a whole year forward.

A school calendar for the year: each school day marked A or B for its rotation day, every Friday in the late-start schedule’s colour, and days off hatched.
Every school day of the year, with its rotation day and bell schedule. Fridays here are late starts. All data is fictional.
The school day: a regular bell schedule from 8:30, Monday to Thursday, and a late-start schedule from 9:00 on Fridays with the same four blocks, lunch and flex, each a little shorter, on one time line from 8:00 to 16:00.
The school day, every bell schedule on one time line: the regular day, and a Friday late start that squeezes the same day into less time. Drag a period to move it. All data is fictional.

At provincial scale

Two fictional districts of a couple of thousand students prove very little. So there’s a load generator, and one run of it filled a database with 394,142 students, 970,813 people and 9.66 million events.

It found a real problem straight away. Under row-level security, the database will only use an index for conditions it knows can’t leak another district’s data, so searches were quietly reading an entire district before filtering. Fixing that brought a name search with a typo from over 100 milliseconds to 40, a student number search to about 10, and a lookup across districts from 412 milliseconds to 8.

Changing my mind

Not everything survived. A family map, with the student in the middle and households around them, looked lovely and was removed, because it showed nothing the list didn’t. Four different calendar designs were built and thrown away for the one above. And for a while, “today” was calculated in UTC, so after five in the afternoon every date field in BC helpfully defaulted to tomorrow.

By the numbers

23commits in about 33½ hours, part-time
~42,700lines of TypeScript, plus about 6,450 of tests
244automated test cases, with accessibility checks on every page

What’s next

Next comes attendance, the first feature built as a separate module that can only use the same public interface as anything else. After that, classes, the gradebook and report cards, which is where the problems in Chapter 2 really start to bite.

It’s early, it’s private, and it’s nowhere near ready for a real school. But it already answers a few of Chapter 2’s problems by design, and it’s been very satisfying to build.

So far, anyway. Ask me again when I get to scheduling.