Skip to content

OneRoster 1.2 is an interchange projection, not the roster core

Status: accepted

After ADR-0001 made QTI 3.0.1 the canonical assessment model, the same move was considered for rostering: adopt OneRoster 1.2 (whose Zod contracts already exist in @conform-ed/contracts) as the storage model for identity, organizations, offerings, and memberships. We decided the opposite: the neutral roster core (person, organization, organization_affiliation, offering, offering_membership, identity_binding) stays canonical; OneRoster is a lossless projection at the boundary plus a completeness checklist for the core.

QTI became canonical because it is semantically richer than anything we would build and the alternative was an unboundedly lossy bidirectional mapping (content trees, response processing). OneRoster is the reverse on both axes:

  • The loss runs the other way. OneRoster collapses six membership states (invited/active/suspended/withdrawn/completed/expired) into active|tobedeleted plus dates, seven offering states into one, and identity-binding history into a flat ID list. Every OneRoster field, by contrast, has (or can trivially gain) a first-class home in the core — emergent→OneRoster is total, OneRoster→emergent loses nothing.
  • OneRoster mandates an institutional skeleton our platform spectrum doesn’t have: a User requires ≥1 org-anchored Role, a Class requires a Course, a school org, and ≥1 term, an Enrollment requires a school. Independent learners, tutors, and rolling-enrollment MOOCs would force fabricated orgs/courses/terms — which would then leak out of our own provider endpoints, degrading the very interop the adoption was meant to buy.
  • The spec has no draft state anywhere, so authoring would need a second non-OneRoster model regardless: the dual-model cost the QTI pivot eliminated would be guaranteed here, not avoided.
  • status: tobedeleted + dateLastModified is itself a polling sync protocol; as storage it would compete with pgxsinkit LWW semantics on read-write synced tables like offering_membership.
  • OneRoster models a mirror of a roster decided elsewhere (the SIS); emergent is the system of record where enrollment is decided. The operator needs states the mirror deliberately omits.
  • The neutral core remains the system of record. OneRoster terms (User, Org, Course, Class, Enrollment) stay out of code, schema, and UI per CONTEXT.md’s one-vocabulary rule; the verbatim terms appear only at the @conform-ed/contracts boundary.
  • The consumer (SIS sync, mid-term per docs/integration/roster-and-grade-exchange.md) follows the existing NRPS pattern (lti_tool_platform / lti_tool_context_mapping / lti_tool_nrps_*_snapshot): retained source snapshots, explicit mapping tables, identity resolution exclusively through identity_binding — OneRoster sourcedIds are bindings, never identities.
  • The provider (REST/CSV) is a projection over the core, emitted only for school-shaped contexts. Conformance certification tests service behavior (filtering, paging, dateLastModified deltas, stable sourcedIds), which a projection satisfies; storage shape is irrelevant to certification.
  • The spec serves as a coverage checklist: concepts it has that we genuinely lack (academic sessions, guardian links, course templates, org hierarchy, grade/subject vocabularies) enter the neutral core as product need arrives — as neutral entities, never as wire shapes.
  • OneRoster as canonical core (mirroring ADR-0001): rejected per the inversion above.
  • Pure connector with no influence on the core: rejected; forfeits the completeness-checklist value that motivated the investigation.
  • Projection totality is the invariant to test: school-context core data must round-trip emergent → OneRoster → emergent losslessly (a CI gate mirroring the QTI round-trip gate from ADR-0001).
  • No Course-template entity is required for the provider: a deterministic Course can be synthesized per Offering at projection time.
  • person.id’s hard FK to auth.users (cascade delete) blocks rostered-but-never-logged-in persons and contradicts the no-destruction lifecycle principle; it must be revisited before any SIS consumer lands (tracked separately). Resolved 2026-06-12 by the person–auth decoupling (docs/architecture/person-auth-decoupling-v1.md): the FK is gone, person.id is minted independently, and auth subjects resolve through identity_binding. The consumer milestone brief is docs/architecture/oneroster-consumer-v1-brief.md.