EngineeringDatabase schema

Database schema

Comprehensive reference for the NexSchool PostgreSQL database, organized by domain. Source of truth is the SQLAlchemy models in server/modules/*/models.py plus the Alembic migrations in server/migrations/versions/ — verify exact column details there when implementing.

  • Database: PostgreSQL 16
  • ORM: SQLAlchemy 2.0 with declarative models

Conventions

These conventions hold across the schema. Individual tables below only note deviations.

  • Multi-tenancy — nearly every table carries a tenant_id foreign key referencing tenants.id. Rows are isolated per tenant; queries are automatically scoped (see /architecture/multi-tenancy). Platform-level tables (tenants, plans, platform_settings) and some rows in audit_logs / notification_templates are the exceptions and may have a null or absent tenant_id.
  • Primary keys — every table uses an auto-increment INTEGER id primary key.
  • Timestamps — time columns are TIMESTAMPTZ (timestamp with time zone). created_at defaults to now() and is NOT NULL; updated_at is set on modification. No local (naive) times are stored.
  • Soft deletes — user-facing entities that support soft delete use a nullable deleted_at TIMESTAMPTZ (null = active). For example, students.deleted_at.
  • Money — all monetary amounts are DECIMAL(12,2). Never FLOAT / DOUBLE.
  • BooleansBOOLEAN columns, typically prefixed is_ / has_, usually with an explicit default.
  • Status fields — stored as TEXT (or VARCHAR) constrained to an enumerated set of allowed values, shown per table.
  • Uniqueness — tenant-scoped natural keys (e.g. roll_number, employee_id, invoice_number) are unique per tenant, not globally.

Platform / multi-tenant

tenants

ColumnTypeNotes
idINTEGERPK, auto-increment
nameTEXTNOT NULL. School name
subdomainTEXTNOT NULL, UNIQUE. URL identifier
statusVARCHARNOT NULL, CHECK. One of active, suspended, trial, cancelled
plan_idINTEGERFK to plans.id. Current plan
feature_flagsJSONBPer-tenant feature overrides
trial_ends_atTIMESTAMPTZNull = no trial
is_setup_completeBOOLEANNOT NULL, DEFAULT false. Setup wizard completed
max_studentsINTEGEROverride from plan
max_teachersINTEGEROverride from plan
contact_emailTEXTSchool contact
phoneTEXTSchool phone
addressTEXTSchool address
logo_urlTEXTSchool logo
created_atTIMESTAMPTZNOT NULL, DEFAULT now()
updated_atTIMESTAMPTZ

plans

ColumnTypeNotes
idINTEGERPK
nameTEXTNOT NULL. One of starter, growth, enterprise
price_monthlyDECIMAL(12,2)
max_studentsINTEGERPer-tenant limit
max_teachersINTEGERPer-tenant limit
features_jsonJSONBArray of feature keys
created_atTIMESTAMPTZ

platform_settings

ColumnTypeNotes
idINTEGERPK
keyTEXTNOT NULL, UNIQUE. Setting key
valueTEXTSetting value
updated_atTIMESTAMPTZ

audit_logs

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERNullable (platform-level actions)
platform_admin_idINTEGERFK to users
actionTEXTNOT NULL. e.g. tenant.create, plan.update
metadataJSONBAction details
created_atTIMESTAMPTZ

Identity & access

users

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK to tenants.id
emailTEXTNOT NULL. Unique per tenant
password_hashTEXTNOT NULL. bcrypt
first_nameTEXT
last_nameTEXT
profile_picture_urlTEXT
is_activeBOOLEANDEFAULT true
is_email_verifiedBOOLEANDEFAULT false
is_platform_adminBOOLEANDEFAULT false
email_verification_tokenTEXT
password_reset_tokenTEXT
password_reset_expires_atTIMESTAMPTZ
last_login_atTIMESTAMPTZ
failed_login_attemptsINTEGERDEFAULT 0
locked_untilTIMESTAMPTZ
created_atTIMESTAMPTZNOT NULL
updated_atTIMESTAMPTZ

Constraint: UNIQUE(email, tenant_id)

sessions

ColumnTypeNotes
idINTEGERPK
user_idINTEGERFK to users.id
token_hashTEXTNOT NULL. Hashed refresh token
expires_atTIMESTAMPTZNOT NULL
is_revokedBOOLEANDEFAULT false
device_infoTEXTUser-agent string
created_atTIMESTAMPTZ

RBAC

permissions

ColumnTypeNotes
idINTEGERPK
resourceTEXTNOT NULL. e.g. student
actionTEXTNOT NULL. e.g. read, create, update, delete
descriptionTEXT

Constraint: UNIQUE(resource, action)

Permission strings follow the resource.action format:

  • student.read, student.create, student.update, student.delete
  • teacher.read, teacher.create, teacher.update, teacher.delete
  • class.read, class.create, class.update, class.delete
  • attendance.read, attendance.mark
  • finance.read, finance.manage
  • timetable.read, timetable.manage
  • transport.read, transport.manage
  • notification.read, notification.send
  • rbac.manage
  • user.manage

roles

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTNOT NULL
descriptionTEXT
created_atTIMESTAMPTZ

role_permissions

ColumnTypeNotes
idINTEGERPK
role_idINTEGERFK to roles.id
permission_idINTEGERFK to permissions.id
tenant_idINTEGERFK

user_roles

ColumnTypeNotes
idINTEGERPK
user_idINTEGERFK to users.id
role_idINTEGERFK to roles.id
tenant_idINTEGERFK
assigned_atTIMESTAMPTZ

People

students

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
user_idINTEGERFK to users.id. Nullable (student may not have a login)
roll_numberTEXTUnique per tenant
admission_numberTEXT
first_nameTEXTNOT NULL
last_nameTEXTNOT NULL
date_of_birthDATE
genderTEXTmale, female, other
blood_groupTEXT
religion_idINTEGERFK to religions.id
nationalityTEXT
class_idINTEGERFK to classes.id
grade_idINTEGERFK to grades.id
medium_idINTEGERFK to mediums.id
parent_nameTEXT
parent_phoneTEXT
parent_emailTEXT
addressTEXT
is_activeBOOLEANDEFAULT true
admission_dateDATE
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ
deleted_atTIMESTAMPTZSoft delete

student_documents

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
student_idINTEGERFK to students.id
document_type_idINTEGERFK to document_types.id
file_nameTEXTOriginal filename
cloudinary_public_idTEXTCloudinary asset ID
cloudinary_urlTEXTSigned URL
file_sizeINTEGERBytes
mime_typeTEXTapplication/pdf, image/jpeg, etc.
uploaded_byINTEGERFK to users.id
created_atTIMESTAMPTZ

student_promotion_batches

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
from_academic_year_idINTEGERFK
to_academic_year_idINTEGERFK
promoted_countINTEGER
created_byINTEGERFK to users.id
created_atTIMESTAMPTZ

teachers

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
user_idINTEGERFK to users.id
employee_idTEXTUnique per tenant
first_nameTEXTNOT NULL
last_nameTEXTNOT NULL
emailTEXT
phoneTEXT
genderTEXT
date_of_birthDATE
joining_dateDATE
departmentTEXT
qualificationTEXT
designationTEXT
is_activeBOOLEANDEFAULT true
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

teacher_leaves

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
teacher_idINTEGERFK to teachers.id
leave_typeTEXTsick, casual, earned
start_dateDATE
end_dateDATE
reasonTEXT
statusTEXTpending, approved, rejected
approved_byINTEGERFK to users.id
created_atTIMESTAMPTZ

teacher_leave_balances

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
teacher_idINTEGERFK
academic_year_idINTEGERFK
leave_typeTEXT
total_daysDECIMAL
used_daysDECIMAL
remaining_daysDECIMALComputed

teacher_availability

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
teacher_idINTEGERFK
day_of_weekINTEGER0 = Monday … 6 = Sunday
periodINTEGERPeriod number
is_availableBOOLEAN
academic_year_idINTEGERFK

Academic structure

academic_years

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTNOT NULL. e.g. 2024-25
start_dateDATE
end_dateDATE
is_currentBOOLEANDEFAULT false
created_atTIMESTAMPTZ

academic_terms

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
academic_year_idINTEGERFK
nameTEXTe.g. Term 1
start_dateDATE
end_dateDATE
created_atTIMESTAMPTZ

grades

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTe.g. Grade 1, Nursery
sequenceINTEGERDisplay order
created_atTIMESTAMPTZ

school_units

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTPhase / section name
codeTEXT
descriptionTEXT
parent_unit_idINTEGERFK, self-referential (hierarchy)
created_atTIMESTAMPTZ

academic_programmes

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTProgramme name
codeTEXT
descriptionTEXT
created_atTIMESTAMPTZ

classes

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTNOT NULL. e.g. 10-A
codeTEXTShort code
grade_idINTEGERFK to grades.id
academic_year_idINTEGERFK to academic_years.id
school_unit_idINTEGERFK to school_units.id
programme_idINTEGERFK to academic_programmes.id
medium_idINTEGERFK to mediums.id
capacityINTEGERMax students
is_activeBOOLEAN
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

class_teachers

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK to classes.id
teacher_idINTEGERFK to teachers.id
subjectTEXTSubject this teacher teaches in this class
is_class_teacherBOOLEANClass teacher / form teacher flag
academic_year_idINTEGERFK

subjects

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTNOT NULL
codeTEXT
descriptionTEXT
subject_context_idINTEGERFK to subject_contexts.id
created_atTIMESTAMPTZ

class_subjects

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK to classes.id
subject_idINTEGERFK to subjects.id
medium_idINTEGERFK
periods_per_weekINTEGER
academic_year_idINTEGERFK

subject_contexts

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTe.g. Science, Languages

mediums

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTe.g. English, Hindi

religions

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXT

holidays

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXT
dateDATE
is_recurringBOOLEANYearly recurrence
academic_year_idINTEGERFK

Attendance

Attendance has two coexisting models: the legacy flat attendance table and the v2 session-based pair (attendance_sessions + attendance_records).

attendance (legacy flat model)

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK to classes.id
student_idINTEGERFK to students.id
dateDATE
statusTEXTpresent, absent, late, excused
marked_byINTEGERFK to users.id
marked_atTIMESTAMPTZ
noteTEXT

Index: idx_attendance_class_date on (class_id, date, tenant_id)

attendance_sessions (v2)

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK
dateDATE
marked_byINTEGERFK to users.id
is_finalizedBOOLEAN
created_atTIMESTAMPTZ

attendance_records (v2)

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
session_idINTEGERFK to attendance_sessions.id
student_idINTEGERFK to students.id
statusTEXTpresent, absent, late
noteTEXT

Finance & fees

fee_structures

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXT
academic_year_idINTEGERFK
is_activeBOOLEAN
created_atTIMESTAMPTZ

fee_structure_classes

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
fee_structure_idINTEGERFK
class_idINTEGERFK
grade_idINTEGERFK (applies to whole grade)

fee_components

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
fee_structure_idINTEGERFK
nameTEXTe.g. Tuition Fee, Transport Fee
amountDECIMAL(12,2)
is_mandatoryBOOLEAN
due_dateDATE
frequencyTEXTmonthly, quarterly, annual

student_fees

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
student_idINTEGERFK
fee_structure_idINTEGERFK
academic_year_idINTEGERFK
total_amountDECIMAL(12,2)
paid_amountDECIMAL(12,2)DEFAULT 0
discount_amountDECIMAL(12,2)DEFAULT 0
balance_amountDECIMAL(12,2)Computed
statusTEXTpaid, partial, unpaid, overdue

student_fee_items

ColumnTypeNotes
idINTEGERPK
student_fee_idINTEGERFK
fee_component_idINTEGERFK
amountDECIMAL(12,2)
paid_amountDECIMAL(12,2)
statusTEXT

fee_invoices

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
student_idINTEGERFK
invoice_numberTEXTUnique per tenant
total_amountDECIMAL(12,2)
paid_amountDECIMAL(12,2)
due_dateDATE
statusTEXTdraft, sent, paid, overdue, cancelled
notesTEXT
created_atTIMESTAMPTZ

fee_invoice_items

ColumnTypeNotes
idINTEGERPK
invoice_idINTEGERFK to fee_invoices.id
descriptionTEXT
amountDECIMAL(12,2)
quantityINTEGER
totalDECIMAL(12,2)

payments

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
student_idINTEGERFK
student_fee_idINTEGERFK
invoice_idINTEGERFK. Nullable
amountDECIMAL(12,2)
payment_dateDATE
payment_methodTEXTcash, online, cheque, dd
reference_numberTEXTTransaction ref
receipt_numberTEXT
collected_byINTEGERFK to users.id
notesTEXT
created_atTIMESTAMPTZ

fee_receipts

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
payment_idINTEGERFK
receipt_numberTEXT
pdf_urlTEXTGenerated PDF URL
created_atTIMESTAMPTZ

Timetable & schedule

timetable_configs

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
academic_year_idINTEGERFK
total_periodsINTEGERPeriods per day
lunch_start_periodINTEGER
lunch_durationINTEGERMinutes
period_durationINTEGERMinutes per period
start_timeTIMESchool start time

timetable_slots

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK to classes.id
subject_idINTEGERFK to subjects.id
teacher_idINTEGERFK to teachers.id
day_of_weekINTEGER0 = Monday … 4 = Friday
periodINTEGERPeriod number (1-based)
academic_year_idINTEGERFK
created_atTIMESTAMPTZ

Constraint: UNIQUE(class_id, day_of_week, period, academic_year_id, tenant_id)

schedule_overrides

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
class_idINTEGERFK
original_slot_idINTEGERFK to timetable_slots.id
dateDATESpecific override date
subject_idINTEGERFK. Replacement subject
teacher_idINTEGERFK. Replacement teacher
is_cancelledBOOLEANCancel this slot
reasonTEXT

Transport

transport_buses

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTBus identifier
registration_numberTEXT
capacityINTEGER
is_activeBOOLEAN

transport_drivers

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXT
phoneTEXT
license_numberTEXT
bus_idINTEGERFK. Currently assigned bus

transport_routes

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXT
bus_idINTEGERFK to transport_buses.id
is_activeBOOLEAN

transport_stops

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
nameTEXTStop name
locationTEXTAddress / coordinates
route_idINTEGERFK to transport_routes.id
sequenceINTEGEROrder on route
pickup_timeTIME
drop_timeTIME

transport_enrollments

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
student_idINTEGERFK to students.id
route_idINTEGERFK to transport_routes.id
stop_idINTEGERFK to transport_stops.id
academic_year_idINTEGERFK
enrollment_typeTEXTpickup, drop, both
is_activeBOOLEAN
enrolled_atTIMESTAMPTZ

transport_staff

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
user_idINTEGERFK
bus_idINTEGERFK
roleTEXTdriver, conductor

transport_bus_assignments

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
bus_idINTEGERFK
route_idINTEGERFK
driver_idINTEGERFK
academic_year_idINTEGERFK

transport_fee_plans

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
route_idINTEGERFK
nameTEXT
amountDECIMAL(12,2)
frequencyTEXT

Notifications

notifications

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
user_idINTEGERFK. Recipient
titleTEXT
messageTEXT
typeTEXTinfo, warning, success
action_urlTEXTDeep link
is_readBOOLEANDEFAULT false
read_atTIMESTAMPTZ
created_atTIMESTAMPTZ

notification_templates

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK. Nullable (platform templates)
nameTEXT
subjectTEXTEmail subject
body_templateTEXTMustache / Jinja template
trigger_eventTEXTe.g. student.created, fee.overdue
channelTEXTemail, push, in_app
is_activeBOOLEAN

device_tokens

ColumnTypeNotes
idINTEGERPK
tenant_idINTEGERFK
user_idINTEGERFK
tokenTEXTNOT NULL. Firebase FCM token
platformTEXTios, android, web
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Relationships overview

High-level ownership graph (──< reads “has many”):

tenants ──< users ──< sessions
   │           │
   │           └──< user_roles >── roles >── role_permissions >── permissions

   ├──< students ──< student_documents
   │       └──< attendance

   ├──< teachers ──< teacher_leaves

   ├──< academic_years ──< academic_terms
   │       │
   │       ├──< classes ──< class_teachers
   │       │       └──< class_subjects
   │       │
   │       ├──< timetable_configs
   │       └──< timetable_slots

   ├──< subjects
   ├──< grades
   ├──< school_units
   ├──< academic_programmes

   ├──< fee_structures ──< fee_components
   │       └──< student_fees ──< payments

   ├──< fee_invoices ──< fee_invoice_items

   ├──< transport_routes ──< transport_stops
   │       └──< transport_enrollments

   └──< notifications