For school administrators

Roster sync and the academic calendar

Roster sync and the academic calendar

Where a school's pupil and staff records come from, when they are refreshed, and what a term start sets in motion. Last updated 2026-09-06.

Read alongside the roster mapping protocol, which says what a vendor is told once these records exist.

1. Why this exists

The roster is the load-bearing thing on the platform. A pupil's externalId is what every vendor keys its accounts on, so a roster that drifts — a leaver who still has access, a pupil in last term's class, a re-import that renumbers everybody — shows up as broken accounts in somebody else's product, weeks later, where nobody can trace it.

Until now the only way in was a CSV upload, done by hand, whenever somebody remembered. This replaces "whenever somebody remembered" with a schedule, a button, and a record of every run.

2. The academic calendar

A school defines its academic years and the terms inside them at School Settings → Academic calendar.

Without one the platform guesses a year from the date — 2026/27 for anything in 2026 — which is right for a January school and wrong for every September one. That guess is the label stamped on every class, and the label a vendor sees, so a school with a real calendar gets real labels and a school without one keeps working on the guess.

Rules the platform enforces: a term sits inside its year, years do not overlap each other, and terms do not overlap each other. A school is in one academic year at a time.

3. What a term start triggers

The start of a term opens a checklist — five tasks, generated once, ticked off by the school:

TaskDueWhy
Bring the roster up to dateday 0ids must stay exactly as they were; a renumbered pupil is a new person to every vendor and nobody can undo it
Check classes and who is in themday 3this is what vendors place people by
End access for leaversday 7their vendor accounts stop the moment they leave the roster
Review which classes use which appsday 7last term's scope carries over silently
Check staff accounts and rolesday 14new staff, changed responsibilities, people who should no longer have access

It is generated, not merely displayed: a checklist that appears and disappears with the date tells nobody afterwards whether the work was done. Where the school has a sync connected, the term start also pulls the roster.

Generation is idempotent on (school, term, task), so a scheduler firing twice, or catching up after an outage, cannot duplicate it.

4. Connecting a student information system

School Settings → Roster sync. One connection per school.

ProviderNeedsState
CSV export over HTTPSthe https URL of the exportworks, and is exercised by tests/sis-sync.test.ts
iSAMShost URL and a Batch API key scoped to pupils and formswritten from published documentation, not yet run against a live school
Wondehttps://api.wonde.com, the school id, the access tokensame

The two unverified adapters say so on the settings page, in those words. An integration that claims to work and does not is discovered by a school at the worst possible moment, and the fix for that is honesty in the interface, not optimism.

Adding a provider is a case in lib/sis/adapters.ts plus a row in lib/sis/providers.ts. Every adapter does the same narrow job — fetch, and return rows in the roster template's shape — so a provider that sends something unexpected produces rejected rows rather than a half-written roster.

No adapter may invent an externalId. A source with no stable pupil code fails the sync outright, with that reason, because a generated id would make every pupil a stranger to every vendor on the very next run.

5. Running a sync

TriggerWhen
Manualsomebody presses *Sync now*, or *Preview*
Scheduleddaily or weekly at an hour the school picks, in UTC
Term startthe first poll of a new term

Deliberately coarse: a roster changes a handful of times a term, and a tight poll loop is a good way to be rate-limited by a school's SIS supplier. The schedule can be off entirely, and the button still works.

Preview changes nothing. It fetches, validates and reports exactly what a real sync would do — added, updated, ended, rejected — and writes not one row. It is the first thing to try against an unverified provider.

What a sync does, and the one thing it never does

  • a pupil in the export who is new here is created;
  • a pupil who exists is updated, keeping their id, and re-enrolled if their class changed;
  • a pupil absent from the export is ended, not deleted.

That last rule is the important one. A bad export — a truncated file, a filter left on, a supplier outage returning an empty list — would otherwise erase a year of history, engagement and audit trail that no vendor can hand back. Ending is reversible: put them back in the export, or edit the record. A sync that ends anybody sends the school a notification saying how many and why.

The record

Every run is a row, successful or not, with the trigger, who pressed it, the counts, and up to fifty rejected rows with their reason. That is what answers *"why did half my pupils change class last night"*, and it is why a failed run is kept rather than swallowed.

Domain events on the school's audit chain: school.sis_configured, school.sis_synced, school.sis_sync_previewed, school.sis_sync_failed, school.sis_disconnected. The credential never appears in any of them — whether one is set is the useful fact — and the Postgres capture trigger masks the column as it does every other secret.

6. Permissions

Two modules (docs/rbac-spec.md):

ModuleVIEWEDIT
school.calendarsee years, terms and the checklistchange them, tick tasks off
school.roster_sync *(sensitive)*see the connection, the schedule and every runchange the connection, run a sync

A teacher holds school.calendar VIEW by default: term dates decide their classes and the checklist is work they do. The sync itself is a school administrator's, and it is separable from the rest of settings so an IT lead can run it without also holding sign-on and subscription.

7. Operations

Point a scheduler at POST /api/internal/roster with header x-audit-key: $AUDIT_SERVICE_SECRET, hourly. It polls every school whose next run has come round and opens the checklist for any term that started in the last day. Both halves are idempotent.

Scheduled in production since 2026-09-06 as eduspaze-roster-poll, hourly (docs/deployment.md §3). A school whose polling is set to *Manual only* is untouched by it; *Sync now* still works for everybody either way.

Source: docs/sis-sync.md in the repository. Last updated with the code it describes.

Roster sync & academic calendar — EduSpaze