OneRoster consumer — design (v1)
Status: implemented (2026-06-12) — CSV lane, then the REST delta lane and the
provider + round-trip gate the same day (see oneroster-provider-v1.md)
Decisions this builds on: ADR-0012 (projection, not roster core), ADR-0013
(person-rooted identity), ADR-0014 (erasure)
Design groundwork: identity-linking-v1.md (implemented — contact points,
binding resolution rules, merge/correlation review), oneroster-consumer-v1-brief.md
Boundary contracts: @conform-ed/contracts oneroster/v1_2 (REST service
schemas + CSV binding)
Decisions (settled 2026-06-12)
Section titled “Decisions (settled 2026-06-12)”- Transport: a transport-neutral ingest core; CSV zip first (admin
upload, external-package-importer pattern). REST
dateLastModifieddelta sync landed later in the milestone on the same reconcile pipeline (oneroster-provider-v1.md→ REST consumer). - Course → Learning Container: deterministic container per SIS course;
Class → Offering linked via
learning_container_id. Correlation onto an existing neutral entity — no wire shape enters the core. - Academic sessions enter the core now as “Learning Period” — the naming the brief left open is settled. Neutral, optional (non-school contexts never touch it), in the Learning Container / Learning Unit family.
- Offering ↔ Learning Period is m:n (junction):
Class.termsis plural and grading periods overlap; the offering keeps its ownstartAt/endAtas the operational window. - Identity rules are inherited, not redesigned: sourcedIds are bindings
(
provider = 'oneroster'); exactly-one service-asserted match auto-binds; zero matches pre-provision (mint person + binding + contact points); ambiguity holds and enqueuesidentity_correlation_review— consumers never mint on ambiguity (identity-linking-v1, “Two review structures”). - Demographics stay at the boundary in v1: retained in snapshots, never projected into the core (no core home; privacy-heavy). Revisit on product need.
- Guardian links deferred:
User.agentsis retained in snapshots; the guardian-relationship core entity (CONTEXT.md → Guardian) lands with the guardian-consent work, not here.
Core additions
Section titled “Core additions”learning_period
Section titled “learning_period”A named, dated span organizing Offerings in time — org-scoped, optional, hierarchical (school year ⊃ semester ⊃ grading period).
| Column | Notes |
|---|---|
id |
uuid PK |
organization_id |
FK → organization, cascade, nullable (corrected during implementation: OneRoster academicSessions carry no org reference — SIS sessions are district-wide. Null = source/platform-scoped; org-scoped when authored in-platform) |
parent_period_id |
self-FK, set null — hierarchy |
title |
text |
kind |
enum school_year | semester | term | grading_period |
starts_on / ends_on |
date (calendar dates, unlike offerings’ timestamps) |
status |
active | archived (no-destruction lifecycle) |
period_meta |
jsonb — wire extensions (e.g. OneRoster schoolYear, custom types) |
| audit columns | auditColumnsUs() |
- Check:
ends_on >= starts_on; index(organization_id, kind, starts_on). - Not person-linked → no erasure declaration (registry covers person-linked tables only). Not client-synced in v1 — no learner/teacher feature reads it yet; joining the pgxsinkit registry is a later, deliberate step.
offering_learning_period
Section titled “offering_learning_period”Junction: offering_id (cascade) + learning_period_id (cascade), unique
pair, audit columns. Consumer-written from Class.terms; platform-authored
offerings may link periods directly later.
Boundary tables (NRPS pattern, schema prefix oneroster_)
Section titled “Boundary tables (NRPS pattern, schema prefix oneroster_)”oneroster_source— the registration (analog oflti_tool_platform): display name, issuer string (used as the binding issuer), transport (csv \| rest), REST credentials columns nullable until the REST lane lands, status, audit. The erasure-relevant actor column iscreated_by_person_id(sever).oneroster_import_batch— one row per ingest run (CSV upload or REST sync window): source FK, manifest/version info, counts per entity, outcome (succeeded \| partial \| failed), error payload, audit. Replaces nothing — this is the traceability anchor the reconciliation principles require.- Per-entity retained snapshots, one row per (batch, sourcedId):
oneroster_org_snapshot,oneroster_academic_session_snapshot,oneroster_course_snapshot,oneroster_class_snapshot,oneroster_user_snapshot,oneroster_enrollment_snapshot,oneroster_demographics_snapshot. Each keeps the validated wire payload (jsonb), the OneRosterstatus+dateLastModified(boundary-only —tobedeletednever leaks into core lifecycle), and resolution columns where applicable (user snapshots: resolution status + person id, NRPS-style; erasure class scrub likelti_tool_nrps_member_snapshot). - Mapping tables (sourcedId → core id, per source):
oneroster_org_mapping(→ organization),oneroster_session_mapping(→ learning_period),oneroster_course_mapping(→ learning_container),oneroster_class_mapping(→ offering). Unique (source, sourcedId); core FK cascade. User mapping needs no table — persons resolve exclusively throughidentity_binding(ADR-0013); enrollments map to memberships viaoffering_membership.metaprovenance (source,sourcedId), mirroring the NRPS membership convention.
Reconcile pipeline (transport-neutral core)
Section titled “Reconcile pipeline (transport-neutral core)”Input: a validated entity batch — parsed and zod-validated at the boundary
(@conform-ed/contracts); CSV v1 parses the zip manifest + files into the same
batch shape REST pages will later produce. Processing order follows the
dependency graph, all within one batch transaction:
- Orgs — every org maps to a flat
organization(create or update via mapping; corrected during implementation from “school-shaped only”: courses reference district-level orgs, so a school-only rule would leave containers unanchored). The parent/child hierarchy is what stays in snapshots only (org hierarchy enters the core on product need, per the checklist). - Academic sessions →
learning_periodrows via session mapping; parent links second pass (parents may appear later in the file). - Courses → deterministic
learning_containerper course via mapping. - Classes →
offeringvia class mapping: title, org, container link;startAt/endAtderived from the linked terms’ date span;offering_learning_periodjunction rows fromClass.terms. - Users → identity resolution per user, in the inherited rule order:
existing
onerosterbinding → resolve; else contact-evidence match (exactly one → bind,sis_email_match); else zero → pre-provision (person + profile fromgivenName/familyName+ binding + contact points from email/sms/phone); else many → hold (no binding, no person) + oneidentity_correlation_reviewrow per candidate (sis_ambiguous). Contact points are upserted from the wire values (source = 'sis-import'). - Enrollments → memberships for resolved users only (held users’ enrollments stay pending in snapshots and reconcile on a later batch once an operator binds); lifecycle mapping below.
Re-running a batch is idempotent: mappings and bindings resolve, snapshots append per batch, memberships reconcile to the latest assertion.
Membership lifecycle mapping (boundary → six states)
Section titled “Membership lifecycle mapping (boundary → six states)”| OneRoster enrollment | offering_membership |
|---|---|
status: active, within beginDate/endDate (or undated) |
active, effective window from dates |
status: active, beginDate in the future |
invited (seat exists, not yet begun), window from dates |
status: active, endDate past |
completed — the SIS asserts it ran to term end |
status: tobedeleted |
withdrawn, effective_until = now() — the OneRoster value itself never enters the row; provenance in meta |
| absent from a full snapshot that previously asserted it | withdrawn (CSV full-file semantics); REST deltas never infer absence |
role: teacher/administrator/proctor → membershipRole teacher (proctor →
assistant); student → learner. Role grants follow the NRPS convention
(offering-scoped grants for teachers).
Provider + round-trip gate (outline — superseded by oneroster-provider-v1.md)
Section titled “Provider + round-trip gate (outline — superseded by oneroster-provider-v1.md)”- Projection over the core for school-shaped contexts: Org ← organization,
Course ← learning_container (synthesized per offering where no container),
Class ← offering, academicSession ← learning_period (synthesized from
offering dates where none linked), User ← person + binding + contact points,
Enrollment ← membership (six states collapse to
active/tobedeleted+ dates — the lossy direction, by design). - Round-trip CI gate (mirrors the QTI gate, ADR-0001): school-shaped core fixtures → provider emits CSV → consumer ingests into a clean schema → compare cores losslessly. CSV-first transport makes this a pure file-fixture lane.
Sequencing
Section titled “Sequencing”- Core schema:
learning_period,offering_learning_period(+ baseline regeneration per the runbook — greenfield mode still applies). - Boundary schema: source, batch, snapshots, mappings (+ erasure declarations for person-linked snapshot/actor columns — CI enforces).
- Ingest core: batch shape, reconcile pipeline, identity resolution reuse.
- CSV transport: zip parsing → validated batch (admin upload endpoint, external-package importer pattern).
- Tests: unit (lifecycle mapping table, batch validation), integration (full CSV fixture through resolve/pre-provision/hold paths, idempotent re-run, withdrawal-by-absence), then the round-trip gate once the provider exists.
Deferred (recorded, not blocking)
Section titled “Deferred (recorded, not blocking)”The deferred-items campaign (2026-06-12/13, oneroster-provider-v1.md →
Deferred-items campaign) cleared most of this list:
REST delta transport— landed; seeoneroster-provider-v1.md(REST delta consumer: watermark on the source, delta mode never infers absence).Provider design proper— landed asoneroster-provider-v1.md(full REST rostering surface + CSV export + round-trip CI gate).Org hierarchy in the core— landed (slice C:organization.kind+parent_organization_id, consumer kind mapping + parent second pass, provider exports parent/children/type, district⊃school in the round-trip gate).Guardian links— landed (slice D: coreperson_guardian; consumer maps wireagentsthrough identity bindings; provider exports guardian users +agentsrefs + roles.csv).— landed (slice F:learning_periodclient synclearning_period+offering_learning_periodjoined the learner sync registry, with period badges on learner-web offering cards).- Demographics projection — decided boundary-only, not deferred (oneroster-consumer-v1 §6, data minimization); endpoints stay conformant-empty until a product need overrides the decision.
- Resources → core projection — boundary-only snapshots in v1
(
oneroster-gradebook-resources-v1-brief.md); projecting vendor-resource pointers awaits a real SIS resource feed. - Teacher-facing
learning_periodUI surfaces — the learner badge landed; teacher gradebook period surfaces are product/UI scope, not spec.