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.
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.
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.
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.
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.
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
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.