System logic

Data schema validation rules

Data schema validation rules

Where each rule lives: schema (Prisma types, enums, uniques), database (triggers, indexes), application (zod schemas in lib/validation/schemas.ts, roster/csv helpers), tests. See docs/schema.md for the data dictionary. Last updated 2026-09-02.

1. Cross-cutting

RuleLayer
Ids are cuid/uuid strings; externalId is [A-Za-z0-9._:-]{1,64} and unique per tenantschema (@@unique([tenantId, externalId])) + zod ExternalId
Emails lower-cased, RFC-shaped, ≤254; unique when presentzod Email + User.email @unique
Usernames ^[a-z0-9][a-z0-9._-]{2,31}$, globally uniquezod Username + User.username @unique
PINs 4–6 digits, never start with 0 when generated; stored bcryptzod Pin, generatePin()
Passwords ≥ 8 chars; stored bcryptzod Password
Validity windows: startDate ≤ now < endDate, endDate null = open, endDate > startDateisActiveOn(), zod refinements, CSV validator
Enumerations are Prisma enums; free strings only where the vocabulary is open and documentedschema
Money is integer SGD with Sgd suffixschema
containsPii/piiSubjectIds are never set by hand — the audited client computes themharness

2. School user management

  • SchoolMembership: (userId, schoolId) unique; role enum; admin cannot end/demote self (action rule).
  • Student: required externalId, firstName, dateOfBirth, yearGroup, className; dateOfBirth valid date ≤ today; at most one open ClassEnrolment (roster helper closes others); className/yearGroup mirror the active enrolment.
  • Class: (tenantId, name, academicYear) unique; academicYear YYYY/YY; startDate required.
  • Group: (tenantId, name) unique.
  • CSV import: header must map all required columns; per-line errors for bad dates, duplicate ids in file, bad username/pin, end ≤ start, pin without username; nothing written on any error (dry run).

3. Identity

  • SsoConfig: provider enum; allowedDomains are ^[a-z0-9.-]+\.[a-z]{2,}$; status transitions DRAFT → VERIFIED only by a successful test sign-in, VERIFIED ↔ ACTIVE by admin; changing provider resets to DRAFT.
  • School.subscriptionStatus enum + subscriptionEndsAt; SSO for non-admins requires ACTIVE|TRIAL and unexpired.
  • SsoIdentity: (provider, subject) unique.

4. Parent module

  • GuardianRelationship: (tenantId, guardianExternalId, childExternalId) unique; parentalResponsibility default false; contactSuppressed default true.
  • TenantParentConfig: thresholds nullable, no defaults; ageOfTransition nullable.
  • GuardianRequest: kind/status enums; body 1–5000 chars.

5. Audit platform

  • Event id unique (idempotent ingest); (chain, chainSeq) unique; hash = SHA-256(prevHash ‖ canonical fields); PII change values encrypted (enc: prefix) under the first subject's key.
  • HMAC signature over timestamp.body; skew ≤ 5 min; batch ≤ 1000 events.
  • No update/delete paths on AuditRecord; SubjectKey.keyEnc is the only field ever nulled (shred).

6. Where validation is *not* yet enforced (tech debt)

Legacy marketplace actions (admin.ts, credits.ts, school.ts, startup.ts, trips.ts) coerce FormData by hand; they are allow-listed in the harness test and must adopt schemas when touched.

Source: docs/data-validation.md in the repository. Last updated with the code it describes.

Data validation — EduSpaze