Skip to content

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)

  1. Transport: a transport-neutral ingest core; CSV zip first (admin upload, external-package-importer pattern). REST dateLastModified delta sync landed later in the milestone on the same reconcile pipeline (oneroster-provider-v1.md → REST consumer).
  2. 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.
  3. 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.
  4. Offering ↔ Learning Period is m:n (junction): Class.terms is plural and grading periods overlap; the offering keeps its own startAt/endAt as the operational window.
  5. 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 enqueues identity_correlation_review — consumers never mint on ambiguity (identity-linking-v1, “Two review structures”).
  6. Demographics stay at the boundary in v1: retained in snapshots, never projected into the core (no core home; privacy-heavy). Revisit on product need.
  7. Guardian links deferred: User.agents is retained in snapshots; the guardian-relationship core entity (CONTEXT.md → Guardian) lands with the guardian-consent work, not here.

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.

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 of lti_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 is created_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 OneRoster status + dateLastModified (boundary-only — tobedeleted never leaks into core lifecycle), and resolution columns where applicable (user snapshots: resolution status + person id, NRPS-style; erasure class scrub like lti_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 through identity_binding (ADR-0013); enrollments map to memberships via offering_membership.meta provenance (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:

  1. 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).
  2. Academic sessionslearning_period rows via session mapping; parent links second pass (parents may appear later in the file).
  3. Courses → deterministic learning_container per course via mapping.
  4. Classesoffering via class mapping: title, org, container link; startAt/endAt derived from the linked terms’ date span; offering_learning_period junction rows from Class.terms.
  5. Users → identity resolution per user, in the inherited rule order: existing oneroster binding → resolve; else contact-evidence match (exactly one → bind, sis_email_match); else zero → pre-provision (person + profile from givenName/familyName + binding + contact points from email/sms/phone); else many → hold (no binding, no person) + one identity_correlation_review row per candidate (sis_ambiguous). Contact points are upserted from the wire values (source = 'sis-import').
  6. 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/proctormembershipRole 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.
  1. Core schema: learning_period, offering_learning_period (+ baseline regeneration per the runbook — greenfield mode still applies).
  2. Boundary schema: source, batch, snapshots, mappings (+ erasure declarations for person-linked snapshot/actor columns — CI enforces).
  3. Ingest core: batch shape, reconcile pipeline, identity resolution reuse.
  4. CSV transport: zip parsing → validated batch (admin upload endpoint, external-package importer pattern).
  5. 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.

The deferred-items campaign (2026-06-12/13, oneroster-provider-v1.md → Deferred-items campaign) cleared most of this list:

  • REST delta transport — landed; see oneroster-provider-v1.md (REST delta consumer: watermark on the source, delta mode never infers absence).
  • Provider design proper — landed as oneroster-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: core person_guardian; consumer maps wire agents through identity bindings; provider exports guardian users + agents refs + roles.csv).
  • learning_period client sync — landed (slice F: learning_period + offering_learning_period joined 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_period UI surfaces — the learner badge landed; teacher gradebook period surfaces are product/UI scope, not spec.