# 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`](database-schema.md); this > file is the **semantic / decision** layer. ## Before you add a model — checklist 1. Does a **built product slice** already cover it? (see "Built slices" below) → reuse it. 2. Does a **reserved SIS table** already model it? (see "Reserved SIS cluster") → wire it, don't recreate. 3. 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. 4. Is it **per-campus configuration / aggregate**? → look at `content_catalog` (global content) or the `campus_*` tables. 5. 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 daily attendance (working `/attendance`) | **`campus_attendance_config` + `campus_attendance_summaries`** | manual **aggregate** entry by `office_manager` (`FILL_ATTENDANCE`). NOT per-student rows. | | Staff attendance | **`staff_attendance_records`** | the staff-attendance slice. | | 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 **and** the daily zone check-in (`item_id` = campus-local date). | | Backend-owned editable content | **`content_catalog`** | global JSONB payloads by `content_type`. | | File upload/download | the **file subsystem** + `file` table | see `file.md`; downloads enforce per-file ownership. | ## 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`). Anchors `classes` and `timetables` to a period. *Plug-in:* set/seed years, mark one `current`. - **`grades`** — grade **levels** (Grade 1, K…: `name`, `code`, `sort_order`). NOT marks. A `class` belongs 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`, and `timetable_periods` hang off it. (So `subjects` = "what"; `class_subjects` = "this offering".) - **`classes`** — a class/group (`name`, `section`, `capacity`, `grade`, `homeroom_teacher`→`staff`, `academic_year`, `campus`). The grouping unit relating teachers (via `class_subjects`), students (via `class_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 a `class_subject`. - **`assessment_results`** — a **student's outcome** (`score`, `grade_letter`, `remarks`) per `assessment` + `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`→`staff`, `class`/`class_subject`). - **`attendance_records`** — a **student's status** in a session (`status` present/absent/late, `minutes_late`) per `studentId`. - Two tables = one session → many student rows. **This is the per-student model and is currently unwired.** The working `/attendance` page uses the **aggregate** `campus_attendance_*` instead, and staff use `staff_attendance_records`. 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 a `class_subject` within a timetable. ### People - **`staff`** — the **employment/HR profile** of a user (`employee_number`, `job_title`, `staff_type`, `hire_date`, `status`, `campus`), linked by `userId`. Distinct from `users` (the **account/identity**: login, email, role, password). One user ↔ one staff profile; students/guardians are users **without** a staff record. Already used by the staff-management and staff-attendance slices. ## 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.