@quad-education/sdk
v0.7.0
Published
The @quad-education/sdk contract, its in-browser engine, transports and contract tests
Readme
@quad-education/sdk
The contract every Quad app is built against. It has no UI framework dependency (D24):
React helpers live in @quad-education/sdk-react. In V1 an in-browser engine implements it inside
the demo shell; the platform implements the same contract later, and both must pass the
contract suite in contract-tests/.
import { connect } from '@quad-education/sdk/client'
const sdk = await connect()
const { items } = await sdk.school.students.list({ where: { yearLevel: 'Y9' } })Modules: auth (who is signed in, and whether the school made them an app admin: me(),
appAdmins(), D133), school (canonical school data, filtered by your scopes and the user's
visibility), db (your app's own data), notify, clock. Slice B adds tasks,
schedule, cron and audit.
Access control for app data (D25)
The engine enforces who can read and write each record. Declare it in the schema:
consent: entity({
fields: {
studentId: ref('school.students'),
status: enumOf(['pending', 'given', 'declined']),
staffNote: text({ optional: true, visibleTo: ['staff'] }),
},
subject: 'studentId',
access: {
staff: { read: 'all', create: 'all', update: 'all', delete: 'all' },
parent: { read: 'subject', update: 'subject' },
student: { read: 'subject' },
},
})subjectnames oneref('school.students')field: the student the record is about.- Rules, per role and operation (
read,create,update,delete):subject: the user can see the record's student (staff: everyone; parents: their own children, never through a restricted link; students: themselves).own: the user created the record.all: every record. Staff only.- An array means any of them. Anything not listed is denied, and an entity with no
accessblock is staff-only.
visibleToon a field: other roles never receive it, can't filter on it and can't write it. Its type is optional unless every role your app serves may see it.- What you get back:
geton a record you can't read:NOT_FOUND, the same as a missing record.list, filters, pages andincludeonly ever cover records you can read.- A denied operation or write:
NOT_PERMITTED. So is settingsubjectto a student you can't see.
- Scheduled and cron jobs run as the app (system principal) with full access to its data.
Pattern: shared record staff-only, per-student rows for parents
subject is a single student in v0. When something concerns many students (an excursion,
a class event), don't give the shared record a subject or parent access. Keep it
staff-only, and create one row per student for anything parents see or do:
excursion: entity({ fields: { title: text(), date: date() } }), // staff-only
attendee: entity({
fields: { excursionId: ref('excursion'), studentId: ref('school.students'), consent: enumOf([...]) },
subject: 'studentId',
access: { staff: { read: 'all', create: 'all', update: 'all', delete: 'all' },
parent: { read: 'subject', update: 'subject' } },
})Parents see and answer only their own child's rows. Copy the few excursion details they
need (title, date) onto the row, or show them from the notification or task that sends
them there. An include of the staff-only excursion returns null for parents.
Querying app data: sorts and ranges need an index (D37)
On the platform, app data lives in DynamoDB, so sdk.db lists follow the same rule everywhere, including the in-browser engine:
- Equality,
inandnefilters work on any field you can see. They only ever scan your app's own data. orderByand range filters (gt,gte,lt,lte) need an index. Declare one in the schema withindexes: [['status', 'dueDate']]. Every entity is also indexed onidandcreatedAt.- An index covers a query when:
- its leading fields have plain equality filters;
- the range filter is on the next field;
- the
orderByfields follow the index in order, all in one direction.
// indexes: [['status', 'dueDate']]
tasks.list({ where: { status: 'open' }, orderBy: { field: 'dueDate' } }) // covered
tasks.list({ where: { status: 'open', dueDate: { lt: today } } }) // covered
tasks.list({ orderBy: { field: 'dueDate' } }) // VALIDATION: needs an equality on status firstThe types allow orderBy and ranges only on indexed fields, so most mistakes are compile errors. The engine checks the rest at runtime and returns VALIDATION with the index to add. sdk.school, tasks.list and audit.list aren't affected.
Versions
0.6.0 (D133, 30 Oct 2026). App admins:
auth.me()gainsappAdminfor a user (staff the school put in charge of the app), andauth.appAdmins()lists them for staff and jobs (FORBIDDEN for a student or parent). The engine gains the platform-onlysetAppAdmins(appId, personIds), the testing kitcreateTestPlatform({ appAdmins })andsetAppAdmins, and the contract tests thesetAppAdminscontrol. A contract change; every@quad-education/*package moves to 0.6.0.0.4.0 (D103, 21 Oct 2026). A small canonical model, and no field-level coverage anywhere an app can see it (a contract change across all the
@quad-education/*packages, which move together). The model is 13 entities and 56 fields: the foundation (schools,students,staff,parents,parentStudentLinks,cohorts,cohortMemberships) and the extended ones some school systems supply (periods,rooms,lessons,sessionAttendance,medicalAlerts,medicalNotes). Removed:enrolments,courses,courseEnrolments,subjects,calendarEventsandsdk.school.calendar,assignments,submissions,results,studentAbsences,attendanceSummaries,behaviours,achievements,learningSupportNeeds,medicalEvents,employeeAbsences,applicantsandpastoralNotes; and fields:students.preferredNameand the seven sensitive demographic fields,staff.positionTitleandphone, a parent's title, mobile, address and language,livesWith,primaryContact,cohorts.parentCohortIdandsubjectId, the register'scommentandminutesLate, a medical alert'sseverityandactionPlan, and a note'stitle,conditionIdandeventId. Added:students.gender,parents.phone,cohorts.subject(text, such as "English"), theformcohort kind, and the capability map (CAPABILITIES,capabilityOf,capabilitiesOf,entityKindin@quad-education/canonical). Changed: apps no longer declare the fields they read, so the manifestdatablock is accepted and ignored (manifestWarningssays to remove it), the engine strips nothing, andcoreis gone; scopes are checked directly. Ids, references and names are required and every other field is typed optional, because a school's system may not supply it. A parent's contact flags (receivesCorrespondence,canConsent,contactRestricted) are optional too, and where a school's system does not supply one the parent is treated as contactable (D17 amended by D103). Thelmsbacking goes, andfieldTier,coreFieldsandvaryingFieldswith it.0.3.0 (D101, 20 Oct 2026). Every app runs in a school, so what 0.2.0 added for apps with no school is removed (a removal across all the
@quad-education/*packages, which move together). Gone from the main entry: theaccountandguestprincipals,ActingAs, the account directory andstaticDirectory, theaccountandguestresults ofauth.me(), theNOT_AVAILABLEerror code (sdk.schoolis always available: the fictional school in dev and the Sandbox, the real school in a school instance), the{ account }recipient, and{ role }taking any string (it is a school role again). Gone from@quad-education/sdk/client:createHttpTransport,connectHttp, the live connection (connectLive,getLiveStatus,signIn,continueAsGuest,SignInRequired) and the live branch ofconnect(), which now only talks to the shell. Gone from@quad-education/sdk/engine(platform-side only):school: false,accounts,accountsFor, a per-apptimezone,attachApp,hasApp,setAccountsandisStandalone. Gone from@quad-education/sdk/contract-tests: theSTANDALONE_APP,ACCOUNT_APPandTEST_ACCOUNTSfixtures and theschooloption.@quad-education/sdk-reactlosesSignInChoicesandSignInActions;@quad-education/schemaloses the manifest'sguests, the reserved roleguestand the SDK-version rule for guests. Thesubjectrule now applies to every app. Kept: thecreatedBy, createdAtimplicit index (a list of one's own records can be sorted), and the engine's state behind anAppStore, so a school instance's data API can run the same rules over DynamoDB; itsinbox,runs,auditLog,effectsSnapshotandrunPrincipalstill return promises. Guest access inside a school instance is wanted, and comes back as a contract change when it is designed. Every earlier version, 0.2.0 included, is withdrawn from npm after this one is out.0.2.0 (20 Oct 2026, withdrawn). Account and guest principals,
NOT_AVAILABLE, the manifestguestsflag and the live connection, for apps that were to run live outside a school (V2c, D100). Removed again by D101 in 0.3.0.0.1.4 (19 Oct 2026). The same SDK calls. The canonical model grows to what V2s S4 builds, which is what an app's
sdk.school.<entity>and its types come from, so 0.1.3 cannot use any of it: the attendance register (sessionAttendance, which must be filtered by student or date;studentAbsences,attendanceSummaries), behaviour and achievements, learning support, medical events and notes, staff absences and admissions applicants, and the demographic fields onstudents. It also adds two sensitivity tiers,sensitiveandhr, which are staff-only (a student or parent never receives them), audited, and need manifest version 1 and a reason. A version 1 manifest still needs 0.1.3 or newer.0.1.3 (18 Oct 2026). The same SDK calls, plus what V2s needs to run: manifest version 1 (
data,reasons,rolesandschoolRoles), declared-field types and stripping for school data, the new school entities (periods,rooms,subjects,lessons, withlessonsgenerated on demand) and the test toolkit that understands all of it. A version 1 manifest needs 0.1.3 or newer: the test toolkit in 0.1.2 only reads version 0 manifests and fails every test withinvalid manifest: manifestVersion: expected 0. The starter templates ask for^0.1.3.0.1.2 (9 Oct 2026). The same contract as 0.1.1, and the first version to stay on npm. All the
@quad-education/*packages were published on 9 Oct 2026 (0.1.0, and 0.1.1 for this one), then unpublished the same day to move the npm org to a new owner, and npm never allows an unpublished version again; every package is now 0.1.2.SDK_VERSION(sdk.version) now matches the package version; it had stayed at 0.1.0 through 0.1.1.0.1.1 (8 Oct 2026). Contract change:
sdk.dblistorderByand range filters need a declared index (orid/createdAt), and both the types and the engine enforce it (D37). The schema DSL keepsindexesas literal types, andDbListOptionsgainsDbWhere,DbOrderByandIndexedField. The packages also moved from@quad/*to@quad-education/*, because the@quadnpm scope belongs to someone else (D41); only the import paths change, and the version stays 0.1.1 because nothing was published under the old name. Before the first publish, the engine also stopped treating names inherited fromObject.prototype(constructor,toString) as entities or fields; they now fail withVALIDATION, as any unknown name does.0.1.0 (renamed 7 Oct 2026). This was
@orca/sdk0.1.0, with the same contract and behaviour, renamed for Quad:- the
@orca/*packages became@quad/*; - the
Orca*exports becameQuad*(QuadError,QuadSdk,QuadAppand the rest); orca genandorca-env.d.tsbecamequad genandquad-env.d.ts;- the handshake and jobs messages became
quad:*.
The version stays 0.1.0 because nothing was published under the old name.
- the
