Database Design¶
Toodle uses PostgreSQL hosted on Supabase, accessed through Prisma ORM by the Express backend. React calls the backend's REST API; the backend owns authorisation, eligibility checks and database operations. Supabase also provides avatar storage, which is separate from the relational data model.
This reference describes the 28 Prisma models in the verified backend origin/develop schema and the connected database metadata inspected on 11 October 2026. Model names refer to Prisma; the inventory gives their mapped PostgreSQL table names. The connected database contains these 28 application tables plus Prisma's internal _prisma_migrations table.
Model Inventory¶
Users and Access¶
| Prisma model | PostgreSQL table | Responsibility |
|---|---|---|
User |
users |
Auth0 identity, profile, application role, onboarding and account state. |
StaffAccessRequest |
staff_access_requests |
A user's staff-access request, status and administrative review. |
StaffInvitation |
staff_invitations |
Email-based Admin/Lecturer invitations, proposed course assignments and expiry. |
AccountClosure |
account_closures |
Durable external cleanup, retry scheduling and completion for a closed account. |
CalendarFeedToken |
calendar_feed_tokens |
Hashed credentials for private, revocable calendar subscriptions. |
Courses and Applications¶
| Prisma model | PostgreSQL table | Responsibility |
|---|---|---|
Course |
courses |
Course catalogue, staffing requirements and application settings. |
CourseCoordinator |
course_coordinators |
Lecturer-to-course assignments and the assigning administrator. |
CourseApplication |
course_applications |
Student/Tutor applications and staff review decisions. |
TutorMark |
tutor_marks |
A user's course mark, verification status and review details. |
Allocations and Scheduling¶
| Prisma model | PostgreSQL table | Responsibility |
|---|---|---|
Allocation |
allocations |
Recurring tutor-to-course assignments, weekly hours, status and locks. |
AllocationDraft |
allocation_drafts |
Saved allocation plans and their creator. |
DraftAllocationEntry |
draft_allocation_entries |
Proposed tutor/course assignments and explanations within a draft. |
CourseSession |
course_sessions |
Recurring teaching sessions, weekdays, times, type and venue. |
Availability |
availability |
A user's recurring free-time windows. |
AvailabilityException |
availability_exceptions |
Dated changes to recurring availability. |
AcademicTerm |
academic_terms |
Teaching blocks that bound session occurrence generation. |
Holiday |
holidays |
Institution-wide non-teaching dates, entered manually or retrieved from Wits. |
Excusal |
excusals |
Requests to miss a session occurrence, with duration and review history. |
SessionSwap |
session_swaps |
Dated exchanges, participant consent, staff decisions and reversal details. |
SessionResponsibility |
session_responsibilities |
Original/current responsibility for an allocation's dated session slot. |
OverflowPost |
overflow_posts |
General volunteering work or cover for a specific missed occurrence. |
OverflowClaim |
overflow_claims |
Volunteer claims and their review outcomes. |
Timesheets and Payments¶
| Prisma model | PostgreSQL table | Responsibility |
|---|---|---|
Timesheet |
timesheets |
Weekly work submission, review state, submission snapshots and applied rate. |
TimesheetEntry |
timesheet_entries |
Dated hours for a course, optionally linked to an allocation/session. |
TutorRate |
tutor_rates |
Effective-dated hourly rate history and corrections. |
CourseBudget |
course_budgets |
A course's budget and recorded spending. |
Notifications and Auditing¶
| Prisma model | PostgreSQL table | Responsibility |
|---|---|---|
Notification |
notifications |
In-app messages, read state, email outcomes and reminder deduplication. |
AuditLog |
audit_logs |
Actor and before/after snapshots of significant entity changes. |
There is no separate payment-transaction model. Timesheet status, rate snapshots and course spending support the current payment-related workflow.
Complete System ERD¶
Open the complete system ERD (SVG) to enlarge and inspect all 28 models. The smaller diagrams below remain the reading guide. The complete diagram includes every live foreign key, the two Prisma-only draft-entry relationships and the legacy course-coordinator foreign key. End labels show cardinalities; dashed and dotted lines distinguish the differences.
Relationships¶
The four diagrams show the current Prisma-declared relationships. || means exactly one, o| means zero or one, and o{ means zero or many. Repeated User, Course and scheduling entities provide context across diagrams. Optional foreign keys are shown explicitly; scalar identifiers and arrays without relational constraints are explained separately.
Users, Staff Access and Course Coordination¶
erDiagram
User ||--o| StaffAccessRequest : requests
User o|--o{ StaffAccessRequest : reviews
User o|--o{ StaffInvitation : issues
User ||--o| AccountClosure : has_cleanup
User ||--o{ CalendarFeedToken : owns
User ||--o{ CourseCoordinator : coordinates
User o|--o{ CourseCoordinator : assigns
Course ||--o{ CourseCoordinator : has_coordinators
Users have the roles ADMIN, LECTURER, TUTOR or STUDENT. Lecturer management access comes from CourseCoordinator membership; it is enforced by backend permissions rather than a foreign key that restricts user roles.
Staff access adds emailVerified and staffOnboardingRole to User. Requests are unique per user; invitations are unique by stored email and expire after seven days under the service's policy. Invitation redemption requires verified Auth0 email matching. StaffInvitation.courseIds is a text array, not a join table: individual course IDs have no foreign keys and are checked by the service.
Account closure adds accountStatus (ACTIVE, CLOSING, CLOSED), closedAt and tokenValidAfter. Its one-to-one cleanup record retains the original Auth0 identifier, cleanup progress and retry/lease information. Closure blocks access and anonymises current profile fields while retaining academic and payroll history; historical snapshots are not comprehensively anonymised. Feed credentials are stored only as SHA-256 hashes, with plaintext returned at creation/rotation.
Course Applications, Allocation Planning and Availability¶
erDiagram
User ||--o{ CourseApplication : applies
User o|--o{ CourseApplication : reviews
Course ||--o{ CourseApplication : receives
User ||--o{ TutorMark : has_marks
User o|--o{ TutorMark : reviews
Course ||--o{ TutorMark : has_marks
User ||--o{ Allocation : receives
User o|--o{ Allocation : assigns
Course ||--o{ Allocation : has_allocations
Course ||--o{ CourseSession : schedules
User ||--o{ AllocationDraft : creates
AllocationDraft ||--o{ DraftAllocationEntry : contains
User ||--o{ DraftAllocationEntry : proposed_tutor
Course ||--o{ DraftAllocationEntry : proposed_course
User ||--o{ Availability : declares
User ||--o{ AvailabilityException : adjusts
AcademicTerm {
string id PK
date startDate
date endDate
}
Holiday {
string id PK
date date
}
Applications, marks and allocations each associate a user with a course. Allocation.sessionIds selects recurring sessions; an empty array means all course sessions. Its elements have no individual session foreign keys, so membership is checked in business logic. Active allocations consume staffing capacity; pending and removed records represent unresolved assignments and retained history.
Each draft belongs to a creator and contains proposed entries. The diagram's tutor/course links for DraftAllocationEntry are declared in Prisma, but those two foreign keys are absent from the inspected PostgreSQL structure; the draft foreign key is present.
Academic terms and holidays are independent calendar records, not foreign-key children of Course or CourseSession. The backend combines them with recurring sessions and availability exceptions to determine valid dates.
Dated Responsibilities, Excusals and Volunteering¶
erDiagram
User ||--o{ Excusal : requests
User o|--o{ Excusal : reviews
Allocation ||--o{ Excusal : concerns
CourseSession o|--o{ Excusal : identifies_session
Excusal o|--o| OverflowPost : generates_cover
Course ||--o{ OverflowPost : offers_work
User ||--o{ OverflowPost : creates
CourseSession o|--o{ OverflowPost : identifies_session
SessionResponsibility o|--o{ OverflowPost : needs_cover
OverflowPost ||--o{ OverflowClaim : receives
User ||--o{ OverflowClaim : volunteers
User o|--o{ OverflowClaim : reviews
User ||--o{ SessionSwap : requests
User o|--o{ SessionSwap : receives
User o|--o{ SessionSwap : reviews
Allocation ||--o{ SessionSwap : origin_allocation
Allocation ||--o{ SessionSwap : target_allocation
CourseSession o|--o{ SessionSwap : origin_session
CourseSession o|--o{ SessionSwap : target_session
Allocation ||--o{ SessionResponsibility : identifies_slot
CourseSession ||--o{ SessionResponsibility : occurs_on
Course ||--o{ SessionResponsibility : belongs_to
User ||--o{ SessionResponsibility : original_tutor
User o|--o{ SessionResponsibility : current_tutor
A responsibility row identifies an allocation, session and date, preserves the scheduled times and records the original and current tutor. A null current tutor can represent a slot awaiting cover; inactive rows represent restored ordinary responsibility. Approved swaps change dated responsibility without exchanging recurring allocation ownership.
New excusals identify an actual session occurrence and preserve its duration. Nullable session/duration fields accommodate older records and preserved history. Approval can create a dated cover post; generic overflow posts retain their recurring-allocation workflow. SessionResponsibility.sourceType/sourceId identify the originating workflow without a direct foreign key to SessionSwap or OverflowClaim. SessionSwap.reversedById is also a scalar identifier rather than a declared User relation.
Timesheets, Rates, Budgets and System Records¶
erDiagram
User ||--o{ Timesheet : submits
User o|--o{ Timesheet : approves
Course o|--o{ Timesheet : scopes
Timesheet ||--o{ TimesheetEntry : contains
Course ||--o{ TimesheetEntry : records_work
Allocation o|--o{ TimesheetEntry : relates_to
CourseSession o|--o{ TimesheetEntry : relates_to
User ||--o{ TutorRate : has_rates
User ||--o{ TutorRate : sets_rates
Course ||--o| CourseBudget : has_budget
User ||--o{ Notification : receives
User o|--o{ AuditLog : acts
A course has zero or one budget, not multiple budgets. Tutor rates belong to users and carry effective dates; Timesheet stores the approved appliedRate value rather than a foreign key to a rate row.
A timesheet may be course-scoped or general, but each entry must reference a course. Allocation and session references are optional, allowing other work such as preparation. Versioned submission snapshots preserve entries and session context for review. Monetary values use fixed-precision decimals; entry hours use Decimal(4,2) and totals use Decimal(5,2).
Notification and audit entity references are generic scalar identifiers, not foreign keys to every possible target. Email status distinguishes provider acceptance (SENT), failure and skipped attempts; it does not confirm inbox delivery.
Database Table Reference¶
The following 28 tables and 282 columns use the inspected PostgreSQL names, types and nullability. Expand a table to inspect its columns. PK and FK describe live database constraints; composite uniqueness applies to the complete listed combination. SQL-only rules are called out separately. UUID generation is performed by Prisma for its String identifiers; PostgreSQL stores them as text.
users ? User
Auth0 identity, profile, application role, onboarding and account state.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
auth0Id |
text | Unique | Auth0 subject identifier. |
email |
text | Unique | Account or invitation email address. |
name |
text ? nullable | ? | Display name. |
role |
"Role" (enum) | ? | Application role. |
studentNumber |
text ? nullable | ? | Student identifier. |
maxHoursPerWeek |
integer | ? | Configured weekly work limit. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
avatarUrl |
text ? nullable | ? | Public avatar URL. |
avatarStorageKey |
text ? nullable | ? | Avatar object key for replacement/cleanup. |
yearOfStudy |
integer ? nullable | ? | Student year of study. |
emailVerified |
boolean | ? | Verified-email state from Auth0. |
staffOnboardingRole |
"Role" (enum) ? nullable | ? | Role whose staff welcome has been completed. |
accountStatus |
"AccountStatus" (enum) | ? | Active, closing or closed account state. |
closedAt |
timestamp without time zone ? nullable | ? | Account closure timestamp. |
tokenValidAfter |
timestamp without time zone ? nullable | ? | Cutoff used to reject older access tokens. |
courses ? Course
Course catalogue, staffing requirements and application settings.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
code |
text | Unique | Unique course code. |
name |
text | ? | Course name. |
description |
text ? nullable | ? | Descriptive text. |
year |
integer | ? | Academic/calendar year. |
semester |
integer | ? | Course semester. |
requiredTutors |
integer | ? | Required tutor count. |
minMarkRequired |
integer | ? | Minimum qualifying course mark. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
applicationsOpen |
boolean | ? | Whether applications are open. |
coordinatorId |
text ? nullable | FK ? users.id (SET NULL) |
Legacy coordinator link retained only in PostgreSQL. |
Prisma difference: legacy coordinatorId and its User foreign key remain in PostgreSQL, outside the current Prisma model. Current application coordination uses course_coordinators.
course_applications ? CourseApplication
Student/Tutor applications and staff review decisions.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
courseId |
text | FK ? courses.id (RESTRICT); Unique with userId |
Reference to courses. |
userId |
text | FK ? users.id (CASCADE); Unique with courseId |
Reference to users. |
hoursPerWeek |
integer | ? | Weekly hours for the assignment or proposal. |
motivation |
text | ? | Applicant motivation. |
status |
"ApplicationStatus" (enum) | ? | Workflow state. |
reviewReason |
text ? nullable | ? | Staff review explanation. |
reviewedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
reviewedAt |
timestamp without time zone ? nullable | ? | Review timestamp. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
SQL-only check: course_applications_hours_check ? CHECK ((("hoursPerWeek" >= 1) AND ("hoursPerWeek" <= 40))).
tutor_marks ? TutorMark
A user's course mark, verification status and review details.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
mark |
integer | ? | Recorded course mark. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
userId |
text | FK ? users.id (CASCADE); Unique with courseId |
Reference to users. |
courseId |
text | FK ? courses.id (CASCADE); Unique with userId |
Reference to courses. |
rejectionReason |
text ? nullable | ? | Explanation for rejection. |
reviewedAt |
timestamp without time zone ? nullable | ? | Review timestamp. |
reviewedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
status |
"TutorMarkStatus" (enum) | ? | Workflow state. |
availability ? Availability
A user's recurring free-time windows.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
dayOfWeek |
"DayOfWeek" (enum) | ? | Recurring weekday. |
startTime |
text | ? | Scheduled start time (HH:MM). |
endTime |
text | ? | Scheduled end time (HH:MM). |
isRecurring |
boolean | ? | Whether availability repeats weekly. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
userId |
text | FK ? users.id (CASCADE) |
Reference to users. |
availability_exceptions ? AvailabilityException
Dated changes to recurring availability.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
date |
date | ? | Calendar date. |
isAvailable |
boolean | ? | Whether the dated exception permits availability. |
startTime |
text ? nullable | ? | Scheduled start time (HH:MM). |
endTime |
text ? nullable | ? | Scheduled end time (HH:MM). |
reason |
text ? nullable | ? | User/staff explanation. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
userId |
text | FK ? users.id (CASCADE) |
Reference to users. |
Lookup indexes: availability_exceptions_userId_date_idx on (userId, date).
academic_terms ? AcademicTerm
Teaching blocks that bound session occurrence generation.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
year |
integer | Unique with name |
Academic/calendar year. |
name |
text | Unique with year |
Teaching-block name. |
startDate |
date | ? | First teaching-block date. |
endDate |
date | ? | Last teaching-block date. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
Lookup indexes: academic_terms_startDate_endDate_idx on (startDate, endDate).
holidays ? Holiday
Institution-wide non-teaching dates, entered manually or retrieved from Wits.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
date |
date | Unique with name |
Calendar date. |
name |
text | Unique with date |
Holiday/non-teaching date name. |
year |
integer | ? | Academic/calendar year. |
source |
text | ? | Source of the holiday record. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
Lookup indexes: holidays_date_idx on (date).
allocations ? Allocation
Recurring tutor-to-course assignments, weekly hours, status and locks.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
hoursPerWeek |
double precision | ? | Weekly hours for the assignment or proposal. |
status |
"AllocationStatus" (enum) | ? | Workflow state. |
reason |
text ? nullable | ? | User/staff explanation. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
userId |
text | FK ? users.id (CASCADE); Unique with courseId |
Reference to users. |
courseId |
text | FK ? courses.id (CASCADE); Unique with userId |
Reference to courses. |
assignedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
isLocked |
boolean | ? | Whether ordinary allocation edits are locked. |
sessionIds |
text[] | ? | Selected session IDs; checked by the backend, without element foreign keys. |
course_sessions ? CourseSession
Recurring teaching sessions, weekdays, times, type and venue.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
dayOfWeek |
"DayOfWeek" (enum) | ? | Recurring weekday. |
startTime |
text | ? | Scheduled start time (HH:MM). |
endTime |
text | ? | Scheduled end time (HH:MM). |
venue |
text ? nullable | ? | Teaching venue. |
sessionType |
"SessionType" (enum) | ? | Lecture, tutorial or lab type. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
courseId |
text | FK ? courses.id (CASCADE) |
Reference to courses. |
allocation_drafts ? AllocationDraft
Saved allocation plans and their creator.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
name |
text | ? | Display name. |
description |
text ? nullable | ? | Descriptive text. |
isPublished |
boolean | ? | Draft publication flag. |
publishedAt |
timestamp without time zone ? nullable | ? | Draft publication timestamp. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
createdById |
text | FK ? users.id (CASCADE) |
Reference to users. |
audit_logs ? AuditLog
Actor and before/after snapshots of significant entity changes.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
action |
"AuditAction" (enum) | ? | Recorded audit action. |
entityType |
text | ? | Audited entity type. |
entityId |
text | ? | Audited entity identifier; no generic foreign key. |
before |
jsonb ? nullable | ? | Previous values as JSON. |
after |
jsonb ? nullable | ? | Resulting values as JSON. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
userId |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
Lookup indexes: audit_logs_entityType_entityId_idx on (entityType, entityId).
course_budgets ? CourseBudget
A course's budget and recorded spending.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
amount |
numeric(10,2) | ? | Course budget amount. |
spent |
numeric(10,2) | ? | Recorded course spending. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
courseId |
text | FK ? courses.id (CASCADE); Unique |
Reference to courses. |
draft_allocation_entries ? DraftAllocationEntry
Proposed tutor/course assignments and explanations within a draft.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
hoursPerWeek |
double precision | ? | Weekly hours for the assignment or proposal. |
reason |
text ? nullable | ? | User/staff explanation. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
draftId |
text | FK ? allocation_drafts.id (CASCADE); Unique with userId, courseId |
Reference to allocation_drafts. |
userId |
text | Unique with draftId, courseId |
Identifier recorded by the workflow. |
courseId |
text | Unique with draftId, userId |
Identifier recorded by the workflow. |
explanation |
jsonb ? nullable | ? | Allocation-generation explanation as JSON. |
Prisma difference: hoursPerWeek is declared Int; live storage is double precision. Tutor/course relations are declared in Prisma but have no live foreign keys.
excusals ? Excusal
Requests to miss a session occurrence, with duration and review history.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
reason |
text | ? | User/staff explanation. |
status |
"ExcusalStatus" (enum) | ? | Workflow state. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
resolvedAt |
timestamp without time zone ? nullable | ? | Workflow resolution timestamp. |
userId |
text | FK ? users.id (CASCADE) |
Reference to users. |
allocationId |
text | FK ? allocations.id (CASCADE); Conditional unique with sessionId, sessionDate |
Reference to allocations. |
sessionId |
text ? nullable | FK ? course_sessions.id (SET NULL); Conditional unique with allocationId, sessionDate |
Reference to course_sessions. |
resolvedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
sessionDate |
date | Conditional unique with allocationId, sessionId |
Dated session occurrence. |
durationMinutes |
integer ? nullable | ? | Snapshot of missed session duration. |
reviewReason |
text ? nullable | ? | Staff review explanation. |
SQL-only partial index: excusals_allocationId_sessionId_sessionDate_key applies where (status <> 'DECLINED'::"ExcusalStatus").
notifications ? Notification
In-app messages, read state, email outcomes and reminder deduplication.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
type |
"NotificationType" (enum) | ? | Notification category. |
message |
text | ? | In-app notification text. |
read |
boolean | ? | Whether the notification was read. |
relatedEntityType |
text ? nullable | ? | Related entity category. |
relatedEntityId |
text ? nullable | ? | Related entity identifier; no generic foreign key. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
userId |
text | FK ? users.id (CASCADE) |
Reference to users. |
title |
text | ? | Notification title. |
emailStatus |
"EmailStatus" (enum) ? nullable | ? | Provider acceptance, failure or skipped attempt. |
emailError |
text ? nullable | ? | Recorded email failure information. |
emailAttemptedAt |
timestamp without time zone ? nullable | ? | Email attempt timestamp. |
dedupeKey |
text ? nullable | Unique | Unique reminder claim key. |
Lookup indexes: notifications_userId_read_idx on (userId, read).
calendar_feed_tokens ? CalendarFeedToken
Hashed credentials for private, revocable calendar subscriptions.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
tokenHash |
text | Unique | SHA-256 subscription credential hash. |
userId |
text | FK ? users.id (CASCADE) |
Reference to users. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
lastFetchedAt |
timestamp without time zone ? nullable | ? | Most recent subscription fetch timestamp. |
Lookup indexes: calendar_feed_tokens_userId_idx on (userId).
overflow_claims ? OverflowClaim
Volunteer claims and their review outcomes.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
status |
"OverflowClaimStatus" (enum) | ? | Workflow state. |
claimedAt |
timestamp without time zone | ? | Volunteer claim timestamp. |
approvedAt |
timestamp without time zone ? nullable | ? | Approval timestamp. |
overflowPostId |
text | FK ? overflow_posts.id (CASCADE); Unique with userId |
Reference to overflow_posts. |
userId |
text | FK ? users.id (CASCADE); Unique with overflowPostId |
Reference to users. |
approvedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
overflow_posts ? OverflowPost
General volunteering work or cover for a specific missed occurrence.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
hoursPerWeek |
double precision | ? | Required work hours; Prisma field is hoursNeeded. |
description |
text ? nullable | ? | Descriptive text. |
status |
"OverflowStatus" (enum) | ? | Workflow state. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
courseId |
text | FK ? courses.id (CASCADE) |
Reference to courses. |
postedById |
text | FK ? users.id (CASCADE) |
Reference to users. |
excusalId |
text ? nullable | FK ? excusals.id (SET NULL); Unique |
Reference to excusals. |
sessionId |
text ? nullable | FK ? course_sessions.id (SET NULL) |
Reference to course_sessions. |
sessionDate |
date ? nullable | ? | Dated session occurrence. |
durationMinutes |
integer ? nullable | ? | Snapshot of missed session duration. |
responsibilityId |
text ? nullable | FK ? session_responsibilities.id (RESTRICT) |
Reference to session_responsibilities. |
session_swaps ? SessionSwap
Dated exchanges, participant consent, staff decisions and reversal details.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
status |
"SwapStatus" (enum) | ? | Workflow state. |
reason |
text ? nullable | ? | User/staff explanation. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
resolvedAt |
timestamp without time zone ? nullable | ? | Workflow resolution timestamp. |
requesterId |
text | FK ? users.id (CASCADE) |
Reference to users. |
requesteeId |
text ? nullable | FK ? users.id (CASCADE) |
Reference to users. |
originAllocationId |
text | FK ? allocations.id (CASCADE) |
Reference to allocations. |
targetAllocationId |
text | FK ? allocations.id (CASCADE) |
Reference to allocations. |
resolvedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
validationWarnings |
jsonb ? nullable | ? | Identifier recorded by the workflow. |
rejectionReason |
text ? nullable | ? | Explanation for rejection. |
reviewedAt |
timestamp without time zone ? nullable | ? | Review timestamp. |
requesteeAcceptedAt |
timestamp without time zone ? nullable | ? | Requested tutor consent timestamp. |
requesterSessionId |
text ? nullable | FK ? course_sessions.id (RESTRICT) |
Reference to course_sessions. |
requesterSessionDate |
date ? nullable | ? | Origin session occurrence date. |
targetSessionId |
text ? nullable | FK ? course_sessions.id (RESTRICT) |
Reference to course_sessions. |
targetSessionDate |
date ? nullable | ? | Target session occurrence date. |
requesterOccurrence |
jsonb ? nullable | ? | Origin occurrence snapshot. |
targetOccurrence |
jsonb ? nullable | ? | Target occurrence snapshot. |
reversedAt |
timestamp without time zone ? nullable | ? | Swap reversal timestamp. |
reversedById |
text ? nullable | ? | Reversing actor identifier; no declared/live foreign key. |
reversalReason |
text ? nullable | ? | Staff reversal explanation. |
session_responsibilities ? SessionResponsibility
Original/current responsibility for an allocation's dated session slot.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
allocationId |
text | FK ? allocations.id (RESTRICT); Unique with sessionId, sessionDate |
Reference to allocations. |
sessionId |
text | FK ? course_sessions.id (RESTRICT); Unique with allocationId, sessionDate |
Reference to course_sessions. |
sessionDate |
date | Unique with allocationId, sessionId |
Dated session occurrence. |
courseId |
text | FK ? courses.id (RESTRICT) |
Reference to courses. |
originalUserId |
text | FK ? users.id (RESTRICT) |
Reference to users. |
responsibleUserId |
text ? nullable | FK ? users.id (RESTRICT) |
Reference to users. |
startTime |
text | ? | Scheduled start time (HH:MM). |
endTime |
text | ? | Scheduled end time (HH:MM). |
active |
boolean | ? | Whether the dated responsibility override is active. |
sourceType |
text | ? | Originating workflow category. |
sourceId |
text | ? | Originating workflow identifier; no generic foreign key. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
Lookup indexes: session_responsibilities_originalUserId_sessionDate_idx on (originalUserId, sessionDate); session_responsibilities_responsibleUserId_sessionDate_idx on (responsibleUserId, sessionDate).
SQL-only check: session_responsibilities_interval ? CHECK (("startTime" < "endTime")).
timesheet_entries ? TimesheetEntry
Dated hours for a course, optionally linked to an allocation/session.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
date |
date | ? | Calendar date. |
hours |
numeric(4,2) | ? | Hours worked in this entry. |
description |
text ? nullable | ? | Descriptive text. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
timesheetId |
text | FK ? timesheets.id (CASCADE) |
Reference to timesheets. |
allocationId |
text ? nullable | FK ? allocations.id (SET NULL) |
Reference to allocations. |
sessionId |
text ? nullable | FK ? course_sessions.id (SET NULL) |
Reference to course_sessions. |
courseId |
text | FK ? courses.id (CASCADE) |
Reference to courses. |
timesheets ? Timesheet
Weekly work submission, review state, submission snapshots and applied rate.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
weekStarting |
date | Unique with userId, courseId; Conditional unique with userId |
Monday starting the timesheet week. |
status |
"TimesheetStatus" (enum) | ? | Workflow state. |
totalHours |
numeric(5,2) | ? | Derived total of entry hours. |
submittedAt |
timestamp without time zone ? nullable | ? | Submission timestamp. |
approvedAt |
timestamp without time zone ? nullable | ? | Approval timestamp. |
disputeNote |
text ? nullable | ? | Staff dispute explanation. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
userId |
text | FK ? users.id (CASCADE); Unique with courseId, weekStarting; Conditional unique with weekStarting |
Reference to users. |
courseId |
text ? nullable | FK ? courses.id (CASCADE); Unique with userId, weekStarting |
Reference to courses. |
approvedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
submittedSnapshot |
jsonb ? nullable | ? | Versioned submitted work and session-context snapshots. |
appliedRate |
numeric(8,2) ? nullable | ? | Effective hourly rate captured at approval; no rate foreign key. |
SQL-only partial index: timesheets_userId_weekStarting_general_key applies where ("courseId" IS NULL).
account_closures ? AccountClosure
Durable external cleanup, retry scheduling and completion for a closed account.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
userId |
text | PK; FK ? users.id (RESTRICT) |
Reference to users. |
requestedAt |
timestamp without time zone | ? | Account cleanup request timestamp. |
completedAt |
timestamp without time zone ? nullable | ? | External cleanup completion timestamp. |
identityRemovedAt |
timestamp without time zone ? nullable | ? | Auth0 identity removal timestamp. |
avatarStorageKey |
text ? nullable | ? | Avatar object key for replacement/cleanup. |
attempts |
integer | ? | Number of cleanup attempts. |
nextAttemptAt |
timestamp without time zone | ? | Next scheduled cleanup attempt. |
leaseUntil |
timestamp without time zone ? nullable | ? | Cleanup worker lease expiry. |
leaseToken |
text ? nullable | ? | Identifier of the worker holding the lease. |
lastErrorCode |
text ? nullable | ? | Safe cleanup failure code. |
auth0Id |
text ? nullable | ? | Original Auth0 subject retained for cleanup. |
Lookup indexes: account_closures_completedAt_nextAttemptAt_idx on (completedAt, nextAttemptAt).
staff_access_requests ? StaffAccessRequest
A user's staff-access request, status and administrative review.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
userId |
text | FK ? users.id (CASCADE); Unique |
Reference to users. |
requestedRole |
"Role" (enum) | ? | Requested staff role. |
reason |
text | ? | User/staff explanation. |
status |
text | ? | Workflow state. |
reviewReason |
text ? nullable | ? | Staff review explanation. |
reviewedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
reviewedAt |
timestamp without time zone ? nullable | ? | Review timestamp. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
updatedAt |
timestamp without time zone | ? | Last update timestamp. |
Lookup indexes: staff_access_requests_status_idx on (status).
SQL-only check: staff_access_requests_role_check ? CHECK (("requestedRole" = ANY (ARRAY['ADMIN'::"Role", 'LECTURER'::"Role"]))).
SQL-only check: staff_access_requests_status_check ? CHECK ((status = ANY (ARRAY['PENDING'::text, 'APPROVED'::text, 'REJECTED'::text, 'CANCELLED'::text]))).
staff_invitations ? StaffInvitation
Email-based Admin/Lecturer invitations, proposed course assignments and expiry.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
email |
text | Unique | Account or invitation email address. |
role |
"Role" (enum) | ? | Application role. |
courseIds |
text[] | ? | Proposed course IDs; array elements have no course foreign keys. |
status |
text | ? | Workflow state. |
expiresAt |
timestamp without time zone | ? | Invitation expiry timestamp. |
acceptedAt |
timestamp without time zone ? nullable | ? | Invitation redemption timestamp. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
createdById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
Lookup indexes: staff_invitations_status_expiresAt_idx on (status, expiresAt).
SQL-only check: staff_invitations_role_check ? CHECK ((role = ANY (ARRAY['ADMIN'::"Role", 'LECTURER'::"Role"]))).
SQL-only check: staff_invitations_status_check ? CHECK ((status = ANY (ARRAY['PENDING'::text, 'ACCEPTED'::text, 'REVOKED'::text]))).
tutor_rates ? TutorRate
Effective-dated hourly rate history and corrections.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
id |
text | PK | Record identifier. |
userId |
text | FK ? users.id (CASCADE); Unique with effectiveFrom |
Reference to users. |
rate |
numeric(8,2) | ? | Hourly pay rate. |
effectiveFrom |
date | Unique with userId |
First effective date for the rate. |
isCorrection |
boolean | ? | Whether this rate was corrected. |
createdById |
text | FK ? users.id (RESTRICT) |
Reference to users. |
createdAt |
timestamp without time zone | ? | Creation timestamp. |
Lookup indexes: tutor_rates_userId_effectiveFrom_idx on (userId, effectiveFrom).
course_coordinators ? CourseCoordinator
Lecturer-to-course assignments and the assigning administrator.
| Column name | PostgreSQL data type | Keys and uniqueness | Description |
|---|---|---|---|
courseId |
text | FK ? courses.id (CASCADE); PK (composite) |
Reference to courses. |
userId |
text | PK (composite); FK ? users.id (CASCADE) |
Reference to users. |
assignedAt |
timestamp without time zone | ? | Coordinator assignment timestamp. |
assignedById |
text ? nullable | FK ? users.id (SET NULL) |
Reference to users. |
Keys, Constraints and Indexes¶
- Primary keys: 26 models use UUID-generated String IDs stored as PostgreSQL text. CourseCoordinator instead has composite primary key
(courseId, userId); AccountClosure uses its user's ID as both primary key and foreign key. - Unique identity and ownership: User email/Auth0 subject, course code, invitation email, request user, budget course and feed-token hash are unique. The feed-token user index is not a uniqueness constraint; rotation is managed by the service.
- Composite uniqueness: user/course pairs for marks, applications and allocations; draft/user/course entries; post/claimant pairs; tutor/effective-date rates; academic year/name; holiday date/name; and allocation/session/date responsibility slots.
- SQL-only rules: a partial unique excusal index applies when status is not
DECLINED, permitting resubmission after decline. A partial timesheet index protects user/week uniqueness when course is null, complementing the course-scoped composite key. These indexes are maintained in migration SQL, not represented as Prisma@@uniquedeclarations. SQL also restricts staff request/invitation roles and statuses and checks responsibility start time precedes end time. - Foreign keys and retention: required and optional links preserve ownership and review context. Many dependent rows cascade on physical deletion; selected reviewer/session links use
SET NULL. SessionResponsibility's allocation, course, session and tutor links, and AccountClosure's user link, useRESTRICT. Application account closure retains User rows rather than relying on cascading deletion. - Lookup indexes: notification user/read state; audit entity/type; availability user/date; responsibility tutor/date; academic date windows; holiday date; staff request status; invitation status/expiry; closure completion/retry date; and tutor/effective-date rates.
- Reminder uniqueness: nullable
Notification.dedupeKeyhas a unique index. Atomic duplicate-skipping insertion determines which job may send the corresponding email.
These constraints do not independently enforce marks, timetable suitability, course membership, weekly limits or reviewer permissions. Those rules are checked by the Express backend.
Validation and Transaction Consistency¶
The shared Serializable transaction helper retries serialization conflicts up to three attempts and returns a conflict when concurrent decisions cannot be reconciled. Allocation writes, application and volunteer approvals, dated swaps and staff-access grants recheck eligibility, permissions and workflow state inside the same transaction as their changes. Draft commitment revalidates selected entries rather than trusting a saved proposal.
Timesheet mutations lock the parent row; creation uses a transaction-scoped advisory lock alongside database uniqueness. Submission snapshots, entry totals, review state, applied pay rate and budget spending are updated within the relevant transaction. Changes to marks, availability or course scheduling also check affected active allocations before committing.
Email and Auth0 cleanup run outside these database transactions. A provider failure cannot roll back an already committed business decision. Account closure persists cleanup work first and uses database leases for retries. See the API Reference for operation contracts.
Schema Management and Verified Database State¶
Prisma's PostgreSQL datasource uses DATABASE_URL and DIRECT_URL. The schema defines application mappings, while version-controlled migrations also contain PostgreSQL-specific indexes, checks and data transformations.
Normal development uses npm run db:migrate (prisma migrate dev). Releases use npm run db:deploy (prisma migrate deploy), followed by npm run db:generate to regenerate the client. These commands change schema or generated code and were not run during the read-only verification. prisma db push is not a replacement for applying the reviewed migration history.
The repository contains 25 migrations, and the connected Supabase database records all 25 as completed. The latest migration by filename is 20261012120000_c04_term_exceptions. Completion records do not prove successful replay on a fresh database, unchanged migration checksums or perfect parity with Prisma.
The metadata comparison found no missing Prisma scalar columns, but confirmed three differences: DraftAllocationEntry.hoursPerWeek is Prisma Int while PostgreSQL stores double precision; the declared draft-entry tutor/course foreign keys are absent; and courses retains a legacy coordinatorId column and User foreign key outside the current Prisma model. These differences remain unresolved. SQL-only partial indexes and checks are additional intentional database rules, not evidence that every Prisma relationship is enforced.
For the wider application structure, see System Overview. Connection and hosting configuration are covered in Deployment and Hosting.