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_idforeign key referencingtenants.id. Rows are isolated per tenant; queries are automatically scoped (see/architecture/multi-tenancy). Platform-level tables (tenants,plans,platform_settings) and some rows inaudit_logs/notification_templatesare the exceptions and may have a null or absenttenant_id. - Primary keys — every table uses an auto-increment
INTEGERidprimary key. - Timestamps — time columns are
TIMESTAMPTZ(timestamp with time zone).created_atdefaults tonow()and isNOT NULL;updated_atis set on modification. No local (naive) times are stored. - Soft deletes — user-facing entities that support soft delete use a nullable
deleted_atTIMESTAMPTZ(null = active). For example,students.deleted_at. - Money — all monetary amounts are
DECIMAL(12,2). NeverFLOAT/DOUBLE. - Booleans —
BOOLEANcolumns, typically prefixedis_/has_, usually with an explicit default. - Status fields — stored as
TEXT(orVARCHAR) 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
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK, auto-increment |
name | TEXT | NOT NULL. School name |
subdomain | TEXT | NOT NULL, UNIQUE. URL identifier |
status | VARCHAR | NOT NULL, CHECK. One of active, suspended, trial, cancelled |
plan_id | INTEGER | FK to plans.id. Current plan |
feature_flags | JSONB | Per-tenant feature overrides |
trial_ends_at | TIMESTAMPTZ | Null = no trial |
is_setup_complete | BOOLEAN | NOT NULL, DEFAULT false. Setup wizard completed |
max_students | INTEGER | Override from plan |
max_teachers | INTEGER | Override from plan |
contact_email | TEXT | School contact |
phone | TEXT | School phone |
address | TEXT | School address |
logo_url | TEXT | School logo |
created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
updated_at | TIMESTAMPTZ |
plans
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
name | TEXT | NOT NULL. One of starter, growth, enterprise |
price_monthly | DECIMAL(12,2) | |
max_students | INTEGER | Per-tenant limit |
max_teachers | INTEGER | Per-tenant limit |
features_json | JSONB | Array of feature keys |
created_at | TIMESTAMPTZ |
platform_settings
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
key | TEXT | NOT NULL, UNIQUE. Setting key |
value | TEXT | Setting value |
updated_at | TIMESTAMPTZ |
audit_logs
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | Nullable (platform-level actions) |
platform_admin_id | INTEGER | FK to users |
action | TEXT | NOT NULL. e.g. tenant.create, plan.update |
metadata | JSONB | Action details |
created_at | TIMESTAMPTZ |
Identity & access
users
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK to tenants.id |
email | TEXT | NOT NULL. Unique per tenant |
password_hash | TEXT | NOT NULL. bcrypt |
first_name | TEXT | |
last_name | TEXT | |
profile_picture_url | TEXT | |
is_active | BOOLEAN | DEFAULT true |
is_email_verified | BOOLEAN | DEFAULT false |
is_platform_admin | BOOLEAN | DEFAULT false |
email_verification_token | TEXT | |
password_reset_token | TEXT | |
password_reset_expires_at | TIMESTAMPTZ | |
last_login_at | TIMESTAMPTZ | |
failed_login_attempts | INTEGER | DEFAULT 0 |
locked_until | TIMESTAMPTZ | |
created_at | TIMESTAMPTZ | NOT NULL |
updated_at | TIMESTAMPTZ |
Constraint: UNIQUE(email, tenant_id)
sessions
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
user_id | INTEGER | FK to users.id |
token_hash | TEXT | NOT NULL. Hashed refresh token |
expires_at | TIMESTAMPTZ | NOT NULL |
is_revoked | BOOLEAN | DEFAULT false |
device_info | TEXT | User-agent string |
created_at | TIMESTAMPTZ |
RBAC
permissions
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
resource | TEXT | NOT NULL. e.g. student |
action | TEXT | NOT NULL. e.g. read, create, update, delete |
description | TEXT |
Constraint: UNIQUE(resource, action)
Permission strings follow the resource.action format:
student.read,student.create,student.update,student.deleteteacher.read,teacher.create,teacher.update,teacher.deleteclass.read,class.create,class.update,class.deleteattendance.read,attendance.markfinance.read,finance.managetimetable.read,timetable.managetransport.read,transport.managenotification.read,notification.sendrbac.manageuser.manage
roles
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | NOT NULL |
description | TEXT | |
created_at | TIMESTAMPTZ |
role_permissions
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
role_id | INTEGER | FK to roles.id |
permission_id | INTEGER | FK to permissions.id |
tenant_id | INTEGER | FK |
user_roles
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
user_id | INTEGER | FK to users.id |
role_id | INTEGER | FK to roles.id |
tenant_id | INTEGER | FK |
assigned_at | TIMESTAMPTZ |
People
students
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
user_id | INTEGER | FK to users.id. Nullable (student may not have a login) |
roll_number | TEXT | Unique per tenant |
admission_number | TEXT | |
first_name | TEXT | NOT NULL |
last_name | TEXT | NOT NULL |
date_of_birth | DATE | |
gender | TEXT | male, female, other |
blood_group | TEXT | |
religion_id | INTEGER | FK to religions.id |
nationality | TEXT | |
class_id | INTEGER | FK to classes.id |
grade_id | INTEGER | FK to grades.id |
medium_id | INTEGER | FK to mediums.id |
parent_name | TEXT | |
parent_phone | TEXT | |
parent_email | TEXT | |
address | TEXT | |
is_active | BOOLEAN | DEFAULT true |
admission_date | DATE | |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ | |
deleted_at | TIMESTAMPTZ | Soft delete |
student_documents
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
student_id | INTEGER | FK to students.id |
document_type_id | INTEGER | FK to document_types.id |
file_name | TEXT | Original filename |
cloudinary_public_id | TEXT | Cloudinary asset ID |
cloudinary_url | TEXT | Signed URL |
file_size | INTEGER | Bytes |
mime_type | TEXT | application/pdf, image/jpeg, etc. |
uploaded_by | INTEGER | FK to users.id |
created_at | TIMESTAMPTZ |
student_promotion_batches
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
from_academic_year_id | INTEGER | FK |
to_academic_year_id | INTEGER | FK |
promoted_count | INTEGER | |
created_by | INTEGER | FK to users.id |
created_at | TIMESTAMPTZ |
teachers
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
user_id | INTEGER | FK to users.id |
employee_id | TEXT | Unique per tenant |
first_name | TEXT | NOT NULL |
last_name | TEXT | NOT NULL |
email | TEXT | |
phone | TEXT | |
gender | TEXT | |
date_of_birth | DATE | |
joining_date | DATE | |
department | TEXT | |
qualification | TEXT | |
designation | TEXT | |
is_active | BOOLEAN | DEFAULT true |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ |
teacher_leaves
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
teacher_id | INTEGER | FK to teachers.id |
leave_type | TEXT | sick, casual, earned |
start_date | DATE | |
end_date | DATE | |
reason | TEXT | |
status | TEXT | pending, approved, rejected |
approved_by | INTEGER | FK to users.id |
created_at | TIMESTAMPTZ |
teacher_leave_balances
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
teacher_id | INTEGER | FK |
academic_year_id | INTEGER | FK |
leave_type | TEXT | |
total_days | DECIMAL | |
used_days | DECIMAL | |
remaining_days | DECIMAL | Computed |
teacher_availability
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
teacher_id | INTEGER | FK |
day_of_week | INTEGER | 0 = Monday … 6 = Sunday |
period | INTEGER | Period number |
is_available | BOOLEAN | |
academic_year_id | INTEGER | FK |
Academic structure
academic_years
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | NOT NULL. e.g. 2024-25 |
start_date | DATE | |
end_date | DATE | |
is_current | BOOLEAN | DEFAULT false |
created_at | TIMESTAMPTZ |
academic_terms
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
academic_year_id | INTEGER | FK |
name | TEXT | e.g. Term 1 |
start_date | DATE | |
end_date | DATE | |
created_at | TIMESTAMPTZ |
grades
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | e.g. Grade 1, Nursery |
sequence | INTEGER | Display order |
created_at | TIMESTAMPTZ |
school_units
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | Phase / section name |
code | TEXT | |
description | TEXT | |
parent_unit_id | INTEGER | FK, self-referential (hierarchy) |
created_at | TIMESTAMPTZ |
academic_programmes
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | Programme name |
code | TEXT | |
description | TEXT | |
created_at | TIMESTAMPTZ |
classes
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | NOT NULL. e.g. 10-A |
code | TEXT | Short code |
grade_id | INTEGER | FK to grades.id |
academic_year_id | INTEGER | FK to academic_years.id |
school_unit_id | INTEGER | FK to school_units.id |
programme_id | INTEGER | FK to academic_programmes.id |
medium_id | INTEGER | FK to mediums.id |
capacity | INTEGER | Max students |
is_active | BOOLEAN | |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ |
class_teachers
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK to classes.id |
teacher_id | INTEGER | FK to teachers.id |
subject | TEXT | Subject this teacher teaches in this class |
is_class_teacher | BOOLEAN | Class teacher / form teacher flag |
academic_year_id | INTEGER | FK |
subjects
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | NOT NULL |
code | TEXT | |
description | TEXT | |
subject_context_id | INTEGER | FK to subject_contexts.id |
created_at | TIMESTAMPTZ |
class_subjects
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK to classes.id |
subject_id | INTEGER | FK to subjects.id |
medium_id | INTEGER | FK |
periods_per_week | INTEGER | |
academic_year_id | INTEGER | FK |
subject_contexts
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | e.g. Science, Languages |
mediums
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | e.g. English, Hindi |
religions
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT |
holidays
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | |
date | DATE | |
is_recurring | BOOLEAN | Yearly recurrence |
academic_year_id | INTEGER | FK |
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)
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK to classes.id |
student_id | INTEGER | FK to students.id |
date | DATE | |
status | TEXT | present, absent, late, excused |
marked_by | INTEGER | FK to users.id |
marked_at | TIMESTAMPTZ | |
note | TEXT |
Index: idx_attendance_class_date on (class_id, date, tenant_id)
attendance_sessions (v2)
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK |
date | DATE | |
marked_by | INTEGER | FK to users.id |
is_finalized | BOOLEAN | |
created_at | TIMESTAMPTZ |
attendance_records (v2)
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
session_id | INTEGER | FK to attendance_sessions.id |
student_id | INTEGER | FK to students.id |
status | TEXT | present, absent, late |
note | TEXT |
Finance & fees
fee_structures
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | |
academic_year_id | INTEGER | FK |
is_active | BOOLEAN | |
created_at | TIMESTAMPTZ |
fee_structure_classes
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
fee_structure_id | INTEGER | FK |
class_id | INTEGER | FK |
grade_id | INTEGER | FK (applies to whole grade) |
fee_components
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
fee_structure_id | INTEGER | FK |
name | TEXT | e.g. Tuition Fee, Transport Fee |
amount | DECIMAL(12,2) | |
is_mandatory | BOOLEAN | |
due_date | DATE | |
frequency | TEXT | monthly, quarterly, annual |
student_fees
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
student_id | INTEGER | FK |
fee_structure_id | INTEGER | FK |
academic_year_id | INTEGER | FK |
total_amount | DECIMAL(12,2) | |
paid_amount | DECIMAL(12,2) | DEFAULT 0 |
discount_amount | DECIMAL(12,2) | DEFAULT 0 |
balance_amount | DECIMAL(12,2) | Computed |
status | TEXT | paid, partial, unpaid, overdue |
student_fee_items
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
student_fee_id | INTEGER | FK |
fee_component_id | INTEGER | FK |
amount | DECIMAL(12,2) | |
paid_amount | DECIMAL(12,2) | |
status | TEXT |
fee_invoices
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
student_id | INTEGER | FK |
invoice_number | TEXT | Unique per tenant |
total_amount | DECIMAL(12,2) | |
paid_amount | DECIMAL(12,2) | |
due_date | DATE | |
status | TEXT | draft, sent, paid, overdue, cancelled |
notes | TEXT | |
created_at | TIMESTAMPTZ |
fee_invoice_items
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
invoice_id | INTEGER | FK to fee_invoices.id |
description | TEXT | |
amount | DECIMAL(12,2) | |
quantity | INTEGER | |
total | DECIMAL(12,2) |
payments
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
student_id | INTEGER | FK |
student_fee_id | INTEGER | FK |
invoice_id | INTEGER | FK. Nullable |
amount | DECIMAL(12,2) | |
payment_date | DATE | |
payment_method | TEXT | cash, online, cheque, dd |
reference_number | TEXT | Transaction ref |
receipt_number | TEXT | |
collected_by | INTEGER | FK to users.id |
notes | TEXT | |
created_at | TIMESTAMPTZ |
fee_receipts
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
payment_id | INTEGER | FK |
receipt_number | TEXT | |
pdf_url | TEXT | Generated PDF URL |
created_at | TIMESTAMPTZ |
Timetable & schedule
timetable_configs
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
academic_year_id | INTEGER | FK |
total_periods | INTEGER | Periods per day |
lunch_start_period | INTEGER | |
lunch_duration | INTEGER | Minutes |
period_duration | INTEGER | Minutes per period |
start_time | TIME | School start time |
timetable_slots
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK to classes.id |
subject_id | INTEGER | FK to subjects.id |
teacher_id | INTEGER | FK to teachers.id |
day_of_week | INTEGER | 0 = Monday … 4 = Friday |
period | INTEGER | Period number (1-based) |
academic_year_id | INTEGER | FK |
created_at | TIMESTAMPTZ |
Constraint: UNIQUE(class_id, day_of_week, period, academic_year_id, tenant_id)
schedule_overrides
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
class_id | INTEGER | FK |
original_slot_id | INTEGER | FK to timetable_slots.id |
date | DATE | Specific override date |
subject_id | INTEGER | FK. Replacement subject |
teacher_id | INTEGER | FK. Replacement teacher |
is_cancelled | BOOLEAN | Cancel this slot |
reason | TEXT |
Transport
transport_buses
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | Bus identifier |
registration_number | TEXT | |
capacity | INTEGER | |
is_active | BOOLEAN |
transport_drivers
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | |
phone | TEXT | |
license_number | TEXT | |
bus_id | INTEGER | FK. Currently assigned bus |
transport_routes
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | |
bus_id | INTEGER | FK to transport_buses.id |
is_active | BOOLEAN |
transport_stops
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
name | TEXT | Stop name |
location | TEXT | Address / coordinates |
route_id | INTEGER | FK to transport_routes.id |
sequence | INTEGER | Order on route |
pickup_time | TIME | |
drop_time | TIME |
transport_enrollments
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
student_id | INTEGER | FK to students.id |
route_id | INTEGER | FK to transport_routes.id |
stop_id | INTEGER | FK to transport_stops.id |
academic_year_id | INTEGER | FK |
enrollment_type | TEXT | pickup, drop, both |
is_active | BOOLEAN | |
enrolled_at | TIMESTAMPTZ |
transport_staff
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
user_id | INTEGER | FK |
bus_id | INTEGER | FK |
role | TEXT | driver, conductor |
transport_bus_assignments
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
bus_id | INTEGER | FK |
route_id | INTEGER | FK |
driver_id | INTEGER | FK |
academic_year_id | INTEGER | FK |
transport_fee_plans
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
route_id | INTEGER | FK |
name | TEXT | |
amount | DECIMAL(12,2) | |
frequency | TEXT |
Notifications
notifications
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
user_id | INTEGER | FK. Recipient |
title | TEXT | |
message | TEXT | |
type | TEXT | info, warning, success |
action_url | TEXT | Deep link |
is_read | BOOLEAN | DEFAULT false |
read_at | TIMESTAMPTZ | |
created_at | TIMESTAMPTZ |
notification_templates
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK. Nullable (platform templates) |
name | TEXT | |
subject | TEXT | Email subject |
body_template | TEXT | Mustache / Jinja template |
trigger_event | TEXT | e.g. student.created, fee.overdue |
channel | TEXT | email, push, in_app |
is_active | BOOLEAN |
device_tokens
| Column | Type | Notes |
|---|---|---|
id | INTEGER | PK |
tenant_id | INTEGER | FK |
user_id | INTEGER | FK |
token | TEXT | NOT NULL. Firebase FCM token |
platform | TEXT | ios, android, web |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ |
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