6.9 KiB
Data Model Guide — purpose, status & reuse
Read this before creating a new model, adding a column, or any DB manipulation (migration/seed). Its job is to prevent reinventing tables that already exist. Many "new feature" needs are already covered by a built slice or by a reserved generated table that should be wired, not duplicated. Column-level detail lives in
database-schema.md; this file is the semantic / decision layer.
Before you add a model — checklist
- Does a built product slice already cover it? (see "Built slices" below) → reuse it.
- Does a reserved SIS table already model it? (see "Reserved SIS cluster") → wire it, don't recreate.
- Is the data per-user self-state? → it likely belongs in
user_progress(e.g. the zone check-in reuses it), not a new table. - Is it per-campus configuration / aggregate? → look at
content_catalog(global content) or thecampus_*tables. - Only if none fit: add a model, following the per-slice template, and update
database-schema.md+ this guide.
Also: every tenant-owned table carries organizationId (+ optional campusId) and is scoped in db/api via findOwnedByPk/tenantWhere; most are paranoid (soft delete). New tables must follow the same conventions.
Built slices — already implemented (do NOT reinvent these)
| Need | Use | Notes |
|---|---|---|
| Staff acknowledge a policy/safety doc | policy_documents + policy_acknowledgments |
per userId × documentId × version, idempotent. Do not repurpose assessments for acknowledgment. |
| Classroom-timer sounds (file/url/recipe) | audio_files |
kind discriminator; recipe = synth params. |
| Campus attendance config / legacy aggregates | campus_attendance_config + campus_attendance_summaries |
config plus legacy aggregate rows. Current /attendance entry is staff-only and does not write student/classroom aggregate rows. |
Staff attendance (working /attendance) |
staff_attendance_records |
active staff-attendance slice for campus, school, and organization attendance rollups. |
| Weekly F.R.A.M.E. entry | frame_entries |
week_of is the canonical Sunday-start ISO (shared/constants/week.ts). |
| Per-user progress / daily self-state | user_progress |
progress_type + item_id + value. Backs sign-learned, Classroom Support favorites, and the daily zone check-in (item_id = campus-local date). |
| Backend-owned editable content | content_catalog |
scoped/editable JSONB payloads by content_type; truly global static catalogs stay in frontend constants. |
| File upload/download | the file subsystem + file table |
see file.md; downloads enforce per-file ownership. |
Organization-level feature flags
organizations.esaEnabled controls whether the ESA Funding Info module is available for an
organization. It defaults to true for existing and newly seeded organizations. Turning it off hides
the frontend ESA module/routes for users in that organization and blocks backend reads/management of
the campus-scoped esa-funding-content payload.
Reserved SIS cluster — kept but not yet wired
These generated tables have no product UI yet. They are retained for future development (owner-approved). When a feature needs one, wire the existing table (build the service/route/UI) — do not create a parallel model. They form a coherent academic/SIS graph:
Academic structure
academic_years— the school year (name,start_date,end_date,current). Anchorsclassesandtimetablesto a period. Plug-in: set/seed years, mark onecurrent.grades— grade levels (Grade 1, K…:name,code,sort_order). NOT marks. Aclassbelongs to a grade.subjects— the reusable subject catalog (Math, English:name,code,description).class_subjects— a subject taught in a class by a teacher (classId+subjectId+teacherId). The many-to-many junction class↔subject;assessments,attendance_sessions, andtimetable_periodshang off it. (Sosubjects= "what";class_subjects= "this offering".)classes— a class/group (name,section,capacity,grade,homeroom_teacher→users,academic_year,campus). The grouping unit relating teachers (viaclass_subjects), students (viaclass_enrollments), and guardians (via their student).class_enrollments— student↔class membership (classId+studentId,enrolled_on,ended_on,status).
Assessments (header/detail pair)
assessments— the definition of a task/exam (name,assessment_type,max_score,due_at,instructions), belongs to aclass_subject.assessment_results— a student's outcome (score,grade_letter,remarks) perassessment+studentId.- Two tables = one-to-many (one assessment → many student results). Academic grading only — not a general "acknowledgment" store (use
policy_acknowledgments).
Attendance (header/detail pair — student-level)
attendance_sessions— one roll-call event for a class (session_date,session_type,taken_by→users,class/class_subject).attendance_records— a student's status in a session (statuspresent/absent/late,minutes_late) perstudentId.- Two tables = one session → many student rows. This is the per-student model and is currently unwired. The working
/attendancepage usesstaff_attendance_records; legacycampus_attendance_*aggregate rows remain separate. If per-student attendance is needed, wire these; do not fold student + staff + aggregate into one model without a deliberate decision.
Scheduling (header/detail pair)
timetables— a schedule version for a campus/year (effective_from,effective_to,status).timetable_periods— individual slots (day_of_week,starts_at,ends_at,room) for aclass_subjectwithin a timetable.
People
users— the account, identity, role, and scope. Do not add a separate employment profile table; employees, students, and guardians are all users with roles and scope columns. Staff classification is derived fromroles.nameplus the user's scope columns.
Pruned — do NOT re-add
students, guardians, fee_plans, invoices, payments, and the old generic
documents entity were removed (this is not a SIS; students/guardians are
roles in roles/users, the finance cluster was unused, and the handbook
moved to policy_documents). Don't recreate them — see docs/backlog.md.
Header/detail rationale
assessments/results, attendance_sessions/records, and
timetables/periods are all the same shape: a parent describes the event
and children hold per-subject rows. Keep that split when wiring — it is what
makes grouping, reporting, and scoping clean.