Class-register assignments and lesson content¶
This chapter is the canonical backend contract for homework, upcoming tests,
and the optional “what we did today” course text. The approved designs are
2026-08-24-class-register-assignments-design.md, its lesson-content
iteration, and the approved
2026-08-25-class-register-assignments-iteration-2-design.md CRUD/Admin
iteration, and the approved
2026-08-25-class-register-assignments-iteration-3-design.md files/links
iteration. The latest iteration is normative where it extends or reverses an
earlier V1 boundary.
1. Storage and ownership¶
Assignment is a mutable informational row for one SubjectGroup in one
academic year. It stores HOMEWORK or TEST, trimmed details, selected
assignedDate, inclusive dueDate, transaction-time createdAt, updatedAt,
frozen course/subject/curriculum/department/grade/homeroom labels, and original
author snapshots. teacherId is a soft nullable UUID: Teacher-created items
expose their creating Teacher, while Admin-created items expose teacher: null
and retain the actual Admin in recordedBy* and audit. There is no recipient,
completion, overdue, submission, status, or notification model.
AssignmentAttachment is the tenant-bearing mixed child wrapper. A FILE
wrapper references one unique File row with usage
ASSIGNMENT_ATTACHMENT and owner (ASSIGNMENT, assignmentId); a LINK
wrapper stores an absolute HTTP/HTTPS URL and optional label. Its database
CHECK makes those branches exclusive. Assignment deletion cascades wrappers,
while File deletion is restricted until its wrapper has been removed. File
blobs retain the shared post-commit, fail-soft cleanup contract.
CourseDayLessonContent is one shared last-commit-wins value per
(tenantId, academicYearId, subjectGroupId, date). It stores frozen
course/subject labels and soft nullable first/latest Teacher snapshots plus
the actual user/name snapshots. Null text means
cleared; the stable row remains as an audit anchor. It is owned by
src/assignments/, not attendance: the attendance frontend composes this API
with its register call, and no attendance record or frozen day slot stores the
text.
All three tenant-bearing tables carry required tenant anchors, restrictive FKs, Class-S
ENABLE/FORCE RLS, tenant-reset coverage, and raw non-blank checks. Assignment
also has dueDate >= assignedDate at the database boundary.
2. Time rules¶
All business dates are YYYY-MM-DD school-calendar dates.
active on T ⇔ assignedDate <= T AND T <= dueDate
assigned on D ⇔ assignedDate = D
due on D ⇔ dueDate = D
Due-today remains active. Passage of time never mutates a row; retained past
items stay in historical day views. Create and update target the active
academic year, forbid a future assigned date, require both dates inside the
course department's calendar, and require dueDate >= assignedDate. Updating
the dates immediately changes active/day projections and can reactivate an
expired item. Explicit hard delete removes the item from every projection.
Neither date must be a timetable occurrence or instructional day.
Lesson content is readable on any admitted course/family day but writable only
when the path date equals tenant schoolToday. A trimmed non-blank string saves
or replaces; null/blank clears; same-value and clear-of-absent requests are
successful no-ops without timestamp or audit noise.
3. Temporal audience¶
Assignments store no recipient rows. Active family visibility uses non-empty
SubjectGroupAssignment effectiveOn(schoolToday) episodes. Once a course is
admitted, all active work is returned even when it predates the membership.
Closed memberships never contribute active work.
For historical assignments, an assignment made on A is admitted by episode
[F,U) when:
There is deliberately no F <= A: after a membership begins, a late joiner
sees the earlier course archive. Multiple episodes union; an open begun episode
dominates closed cutoffs. A due-day may remain visible after exit when the
assignment itself was made before the exclusive cutoff.
Lesson content applies the same retrospective rule to its own date D:
U IS NULL OR D < U. Cleared values are omitted from the family array.
Student sessions are self-only. Referents additionally need the current
non-temporal StudentReferentLink; canWrite is irrelevant. Linking exposes
the student's permitted archive immediately, and unlinking removes it.
Participation (2026-09-03). GET /assignments/family/active enumerates
the caller's children, so its access predicate is the family link ∧ the
live fence status = ENROLLED (participatingStudentWhere): a child who has
left, or one not yet enrolled, contributes no active work. The by-id day read
and the attachment download keep the bare link, so an exited child's archive
stays readable — the membership cutoff rules above are unchanged. Design:
2026-09-03 spec.
4. API¶
Fourteen protected routes exist:
| Verb | Route | Contract |
|---|---|---|
| POST | /assignments |
Create one assignment from JSON, or atomically create it with repeated files and JSON-encoded links multipart parts |
| GET | /assignments/:assignmentId |
Read one active-year school-side assignment |
| PATCH | /assignments/:assignmentId |
Change type, details, assigned date, and/or due date |
| DELETE | /assignments/:assignmentId |
Hard-delete one assignment; 204 empty response |
| GET | /assignments/subject-groups/:subjectGroupId/days/:date |
Course header, nullable lessonContent, assigned[], due[] |
| PUT | /assignments/subject-groups/:subjectGroupId/days/:date/lesson-content |
Replace/clear today's shared course text |
| GET | /assignments/family/active?studentId= |
{ asOf, items[] }, each item explicitly names its student |
| GET | /assignments/family/students/:studentId/days/:date |
Student header, lessonContents[], assigned[], due[] |
| POST | /assignments/:assignmentId/attachments/files |
Append one atomic multipart file batch |
| POST | /assignments/:assignmentId/attachments/links |
Append one atomic JSON link batch |
| PATCH | /assignments/:assignmentId/attachments/links/:attachmentId |
Correct a link URL and/or label |
| DELETE | /assignments/:assignmentId/attachments/:attachmentId |
Hard-delete one FILE or LINK wrapper; 204 empty response |
| GET | /assignments/:assignmentId/attachments/:attachmentId/download |
School-side signed URL for a FILE wrapper |
| GET | /assignments/family/students/:studentId/assignments/:assignmentId/attachments/:attachmentId/download |
Student/Referent signed URL after the family parent fence is re-proved |
AssignmentDto exposes id, type, details, frozen course/subject/homeroom refs,
a nullable original Teacher ref, assigned/due dates, createdAt, and
updatedAt, and required attachments[]. FILE items expose wrapper id,
name, normalized MIME, byte size, and wrapper timestamps. LINK items expose
wrapper id, URL, nullable label, and wrapper timestamps. The DTO exposes no
internal File id, storage key, uploader, or signed URL. It exposes no internal actor, organizational snapshots,
recipients, or derived state. PATCH accepts a non-empty subset of type,
details, assignedDate, and dueDate; course and original authorship are
immutable. A normalized no-op preserves updatedAt. A same-day assignment
appears once in each day bucket. Day buckets order by createdAt, then id;
active items order by due date, assigned date, student last/first name, then
assignment id. Lesson contents order by subject name, course name, then id.
One Assignment may contain at most ten FILE/LINK items combined. Each file is
at most 3,145,728 bytes and persisted files total at most 15,728,640 bytes;
links consume an item but no byte budget. The exact case-insensitive file
extensions are .pdf, .docx, .xls, .xlsx, .csv, .jpg, .jpeg, and
.png; persisted MIME is normalized from the extension (.jpg/.jpeg to
image/jpeg and .png to image/png). Links are inert data: absolute
HTTP/HTTPS, maximum 2048 characters, with a nullable trimmed/nonblank label of
at most 120 characters. Duplicates are allowed. Wrappers order by createdAt,
then id.
Direct multipart create validates the course, dates, authority, every file,
and aggregate caps before storage. It pre-mints the Assignment id, stages only
blobs, then creates Assignment, File rows, wrappers, links, and the one
assignment.created audit event in the aggregate transaction. Failure
compensation-deletes staged blobs. Every post-create child mutation locks the
parent and re-counts the persisted aggregate before writing. A material child
change touches parent updatedAt; a normalized link no-op does not. Passed-due
items remain correctable through their historical parent authority.
Missing, foreign, hidden course/student anchors use generic 404. Missing,
foreign, archived-year, hidden, or deleted assignment ids use
ASSIGNMENT_NOT_FOUND. Other semantic errors are
ASSIGNMENT_NOT_AUTHORIZED_FOR_COURSE,
ASSIGNMENT_ASSIGNED_DATE_IN_FUTURE,
ASSIGNMENT_DATE_OUTSIDE_CALENDAR, ASSIGNMENT_DUE_BEFORE_ASSIGNED,
ASSIGNMENT_ATTACHMENT_NOT_FOUND, ASSIGNMENT_ATTACHMENT_LIMIT,
ASSIGNMENT_ATTACHMENT_TOTAL_TOO_LARGE,
LESSON_CONTENT_NOT_AUTHORIZED_FOR_COURSE, and
LESSON_CONTENT_DATE_NOT_TODAY.
5. Authorization and audit¶
The flat assignments entity has descriptor scope record. create,
delete, and set_lesson_content each require record:WRITE; PATCH uses
record:WRITE directly. Teacher holds WRITE and all three actions. Admin
inherits the same command capabilities and is tenant-wide within the active
academic year without needing a Teacher profile. Student/referent hold READ;
principal/secretary and qualified department/curriculum management read their
course reach. HR and generic staff receive no assignment scope.
Teacher creation requires an active-year active Teacher profile and a
non-empty teaching episode effective on assignedDate. Teacher update/delete
requires a teaching episode on the stored assigned date; a changed assigned
date also requires authority on the proposed date. Co-teachers satisfying the
same course/date predicate may mutate the shared item. Teacher lesson-content
writes require the episode today. Admin bypasses only these Teacher predicates
and may create, update, delete, or set today's lesson content for any
active-year SubjectGroup. Dated school reads admit whole-school management,
qualified immutable course anchors, or a Teacher episode effective on the
relevant date.
Attachment create/update/delete reuses assignments.record:WRITE: Admin is
active-year tenant-wide and Teacher must have the same effective teaching
episode on the parent stored assigned date. There is no new RBAC primitive.
Metadata inherits every existing parent projection. School download reuses the
school parent READ fence; family download additionally names the relevant
student and re-proves that student's retrospective audience before minting.
Wrong-kind, wrong-parent, foreign, and hidden attachment ids use the same
existence-hiding 404 posture.
Assignment creation, material correction, deletion, and every material lesson-content transition append audit inside the owning transaction:
assignment.createdassignment.updatedwith complete before/after business snapshotsassignment.deletedwith the complete pre-delete business snapshotassignment.attachments_addedfor one post-create batchassignment.attachment_updatedwith public before/after link metadataassignment.attachment_deletedwith public removed-item metadatacourse_day_lesson_content.createdcourse_day_lesson_content.updatedcourse_day_lesson_content.cleared
Reads, deadline passage, membership changes, and idempotent writes add no event. Delete is physical and has no restore API; its audit row survives.
6. V1 boundary and deployment¶
There is no rich text, file replacement/rename/reorder, link preview/fetch,
malware/content scanner, completion, overdue badge, reminder, restore,
revision/history endpoint, target-course update, per-period content, or
CombinedClass/activity/homeroom/grade target. Deploy both assignment
migrations plus 20260825150000_assignment_attachments in order and regenerate
Prisma before enabling attachment clients. No role reseed is required for the
iteration-3 child routes because they reuse the existing record scope.