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.
Why the QTI precedent inverts
Section titled “Why the QTI precedent inverts”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|tobedeletedplus 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+dateLastModifiedis itself a polling sync protocol; as storage it would compete with pgxsinkit LWW semantics on read-write synced tables likeoffering_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.
Decision
Section titled “Decision”- 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/contractsboundary. - 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 throughidentity_binding— OneRostersourcedIds 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,
dateLastModifieddeltas, 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.
Considered and rejected
Section titled “Considered and rejected”- 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.
Consequences
Section titled “Consequences”- 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 toauth.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.idis minted independently, and auth subjects resolve throughidentity_binding. The consumer milestone brief isdocs/architecture/oneroster-consumer-v1-brief.md.