Database Schema
Complete reference for the Exam Portal database structure powered by Turso (libSQL).
Overview
The database implements a multi-tenant schema with support for comprehensive exam lifecycle management, subscription billing, Pay Per Test system, and audit tracking.
Entity Relationship Diagram
The database contains the following key entity groups:
Core Tenant Tables
- clients: Organization/tenant management
- profiles: User account information
- user_roles: Role assignments with multi-role support
Question Management Tables
- questions: Question bank with versioning and bulk import support
- question_folders: Category folders for organizing questions
- question_import_logs: Audit trail for CSV question imports
Test Management Tables
- tests: Exam configurations with comprehensive settings
- test_folders: Folder-based test organization
- test_sections: Section-based test organization with per-section configuration
- test_questions: Linking questions to tests with section and position
Exam Execution Tables
- attempts: Student test attempts with status tracking
- attempt_answers: Student answers with auto-save support
Proctoring Tables
- proctoring_events: Security event logging with evidence storage
Subscription & Billing Tables
- subscription_plans: Predefined subscription tiers (Free, Starter, Growth, Enterprise)
- subscription_plan_features: Feature matrix for subscription plans
- client_subscriptions: Active subscription assignments
- subscription_history: Audit trail for plan changes
- client_subscription_requests: Upgrade request workflow
Pay Per Test Tables
- test_packages: Assessment package catalog (₹99-₹1,499)
- client_test_purchases: Package inventory and consumption tracking
- test_billing: Links tests to consumed packages with locked limits
Support Tables
- client_limits: Manual limit overrides for clients
- client_features: Feature flag overrides for clients
- client_usage_monthly: Monthly resource consumption tracking
- global_settings: Platform-wide configuration
- audit_logs: Security and compliance audit trail
Core Tables
Clients
Organization/tenant management.
CREATE TABLE clients (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
logo_url TEXT,
active_status INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Super Admins can suspend clients by setting
active_status = 0 - Suspended clients cannot create new tests or attempts
- Logo URL used for branding in dashboards and reports
Profiles
User profiles linked to Firebase Authentication UIDs.
CREATE TABLE profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
client_id TEXT REFERENCES clients(id),
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Super Admins have
client_id = NULL - Guest users have temporary profiles with
guest_prefix emails - Email uniqueness not enforced (Firebase handles this)
User Roles
Role assignments with multi-role support.
CREATE TABLE user_roles (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES profiles(id),
role TEXT NOT NULL CHECK(role IN ('superadmin', 'clientadmin', 'student')),
client_id TEXT REFERENCES clients(id)
);
Business Rules:
- Users can have multiple roles, but system uses highest priority
- Role hierarchy:
superadmin>clientadmin>student - Super Admins can have
client_id = NULL
Question Folders
Organize questions into hierarchical folders.
CREATE TABLE question_folders (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
name TEXT NOT NULL,
parent_id TEXT REFERENCES question_folders(id),
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Questions
Question bank with versioning and bulk import support.
CREATE TABLE questions (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
folder_id TEXT REFERENCES question_folders(id),
question_text TEXT NOT NULL,
question_type TEXT DEFAULT 'mcq',
options TEXT DEFAULT '[]',
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
option_c TEXT,
option_d TEXT,
correct_answer TEXT,
correct_answers TEXT DEFAULT '[]',
marks INTEGER DEFAULT 1,
negative_marks REAL DEFAULT 0,
difficulty TEXT DEFAULT 'medium',
explanation TEXT DEFAULT '',
is_case_sensitive INTEGER DEFAULT 0,
import_batch_id TEXT,
version INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Question Types:
mcq: Multiple choice (single correct answer A-D)true_false: True/False (A=True, B=False)multi_select: Multiple correct answers (JSON array format)fill_blank: Text input with case sensitivity optionsubjective: Essay/long answercoding: Programming questions (future enhancement)
Business Rules:
- Questions are tenant-scoped by
client_id - Versioning: editing copies old version, old exams keep reference
- Bulk import:
import_batch_idallows rollback of entire imports
Question Import Logs
Track CSV import operations.
CREATE TABLE question_import_logs (
id TEXT PRIMARY KEY,
uploaded_by TEXT,
uploaded_at TEXT,
import_batch_id TEXT,
total_questions INTEGER,
imported_count INTEGER,
duplicate_count INTEGER,
failed_count INTEGER
);
Tests
Exam configurations with comprehensive settings.
CREATE TABLE tests (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
folder_id TEXT REFERENCES test_folders(id),
test_name TEXT NOT NULL,
timer INTEGER NOT NULL,
shuffle INTEGER DEFAULT 0,
allow_review INTEGER DEFAULT 0,
negative_marking INTEGER DEFAULT 0,
negative_marks REAL DEFAULT 0,
restrict_navigation INTEGER DEFAULT 0,
attempts_allowed INTEGER DEFAULT 1,
status TEXT DEFAULT 'draft',
active INTEGER DEFAULT 1,
allow_guests INTEGER DEFAULT 0,
scheduled_start TEXT,
scheduled_end TEXT,
share_code TEXT UNIQUE,
public_link_enabled INTEGER DEFAULT 0,
show_results_after_submission INTEGER DEFAULT 0,
allow_report_download INTEGER DEFAULT 0,
result_status TEXT DEFAULT 'draft',
camera_required INTEGER DEFAULT 0,
read_only INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Tests must be
publishedandactivefor students to attempt - Scheduled windows enforced at attempt creation
share_coderegenerated on clone, unique across platformread_onlyset when Pay Per Test capacity exhausted- Timer is in minutes
Test Folders
Organize tests into folders.
CREATE TABLE test_folders (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Test Sections
Section-based test organization with per-section configuration.
CREATE TABLE test_sections (
id TEXT PRIMARY KEY,
test_id TEXT NOT NULL REFERENCES tests(id),
name TEXT NOT NULL,
position INTEGER DEFAULT 0,
duration_minutes INTEGER,
negative_marks REAL DEFAULT 0,
shuffle_questions INTEGER DEFAULT 0,
shuffle_options INTEGER DEFAULT 0,
navigation_locked INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Sections allow different timers per section
- Navigation lock prevents returning to completed sections
- Section negative marks override test-level setting
- Option shuffling is client-side with mapping preservation
Test Questions
Linking questions to tests with section and position.
CREATE TABLE test_questions (
id TEXT PRIMARY KEY,
test_id TEXT NOT NULL REFERENCES tests(id),
question_id TEXT NOT NULL REFERENCES questions(id),
section_id TEXT REFERENCES test_sections(id),
position INTEGER
);
Attempts
Student test attempts.
CREATE TABLE attempts (
id TEXT PRIMARY KEY,
student_id TEXT NOT NULL REFERENCES profiles(id),
test_id TEXT NOT NULL REFERENCES tests(id),
score REAL,
total_marks REAL,
started_at TEXT,
submitted_at TEXT,
time_taken INTEGER,
status TEXT DEFAULT 'in_progress',
ip_address TEXT,
attempt_token TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
attempt_tokengenerated for guest attempts, required for accessstarted_atandsubmitted_attrack attempt duration- Score hidden unless
show_results_after_submission = 1ANDresult_status = 'published' - Only one
in_progressattempt per student per test allowed
Attempt Answers
Student answers tracking.
CREATE TABLE attempt_answers (
id TEXT PRIMARY KEY,
attempt_id TEXT NOT NULL REFERENCES attempts(id),
question_id TEXT NOT NULL REFERENCES questions(id),
selected_option TEXT,
marked_for_review INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- UNIQUE constraint on
(attempt_id, question_id)prevents duplicates - Auto-save every 2 seconds with debouncing
- Answers preserved even if test becomes
read_only
Proctoring Events
Security event logging with evidence storage.
CREATE TABLE proctoring_events (
id TEXT PRIMARY KEY,
attempt_id TEXT NOT NULL REFERENCES attempts(id),
test_id TEXT NOT NULL REFERENCES tests(id),
event_type TEXT NOT NULL,
severity TEXT NOT NULL,
severity_score INTEGER DEFAULT 0,
storage_path TEXT,
has_evidence INTEGER DEFAULT 0,
metadata TEXT,
duration_seconds REAL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Event Types:
| Event Type | Severity | Score | Evidence Required |
|---|---|---|---|
TAB_SWITCH | LOW | 1 | No |
WINDOW_BLUR | LOW | 1 | No |
FULLSCREEN_EXIT | MEDIUM | 2 | No |
NO_FACE | MEDIUM | 3 | Yes |
MULTIPLE_FACES | HIGH | 5 | Yes |
CAMERA_DISCONNECTED | HIGH | 5 | Yes |
CAMERA_PERMISSION_DENIED | HIGH | 5 | No |
Business Rules:
- 30-second deduplication window for repeated events
- Evidence images stored in Firebase Storage with signed URLs
- Risk score accumulated per attempt
- High-risk events can trigger auto-submission
Subscription & Billing Tables
Subscription Plans
Predefined subscription tiers.
CREATE TABLE subscription_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
monthly_price REAL,
features TEXT,
max_exams_per_month INTEGER DEFAULT -1,
max_students_per_exam INTEGER DEFAULT -1,
max_questions_per_exam INTEGER DEFAULT -1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Default Plans:
| Plan | Price | Exams/Month | Students/Exam | Questions/Exam |
|---|---|---|---|---|
| Free | ₹0 | 3 | 20 | 50 |
| Starter | ₹1,999/mo | 25 | 100 | 100 |
| Growth | ₹3,999/mo | 50 | 250 | 200 |
| Enterprise | Custom | Unlimited | Unlimited | Unlimited |
Subscription Plan Features
Feature matrix for subscription plans.
CREATE TABLE subscription_plan_features (
plan_id TEXT REFERENCES subscription_plans(id),
feature_name TEXT,
PRIMARY KEY (plan_id, feature_name)
);
Features: csv_import, xlsx_export, analytics, custom_branding, advanced_proctoring, camera_proctoring
Client Subscriptions
Active subscription assignments.
CREATE TABLE client_subscriptions (
client_id TEXT PRIMARY KEY REFERENCES clients(id),
plan_id TEXT NOT NULL REFERENCES subscription_plans(id),
status TEXT DEFAULT 'active',
start_date TEXT,
end_date TEXT,
renewal_status TEXT CHECK(renewal_status IN ('auto_renew', 'manual')),
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Expiry checked on Super Admin page load (lazy expiration)
- Expired subscriptions block new test creation
- Status changes logged in
subscription_history
Subscription History
Audit trail for plan changes.
CREATE TABLE subscription_history (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
old_plan_id TEXT REFERENCES subscription_plans(id),
new_plan_id TEXT NOT NULL REFERENCES subscription_plans(id),
changed_by TEXT,
changed_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Client Subscription Requests
Upgrade request workflow.
CREATE TABLE client_subscription_requests (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
plan_id TEXT NOT NULL REFERENCES subscription_plans(id),
status TEXT DEFAULT 'pending',
requested_at TEXT DEFAULT CURRENT_TIMESTAMP,
actioned_at TEXT
);
Pay Per Test Tables
Test Packages
Assessment package catalog.
CREATE TABLE test_packages (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
max_questions INTEGER NOT NULL,
max_candidates INTEGER NOT NULL,
csv_import INTEGER DEFAULT 0,
xlsx_export INTEGER DEFAULT 0,
analytics INTEGER DEFAULT 1,
custom_branding INTEGER DEFAULT 0,
basic_proctoring INTEGER DEFAULT 0,
camera_proctoring INTEGER DEFAULT 0,
priority_support INTEGER DEFAULT 0,
active INTEGER DEFAULT 1
);
Default Packages:
| Package | Price | Questions | Candidates | Features |
|---|---|---|---|---|
| Base | ₹99 | 50 | 50 | Analytics, Branding |
| Basic | ₹199 | 50 | 50 | CSV, XLSX, Basic Proctoring |
| Standard | ₹399 | 50 | 50 | Camera Proctoring |
| Professional | ₹499 | 100 | 100 | All features |
| Placement Drive | ₹1,499 | 200 | 500 | Full camera proctoring |
Client Test Purchases
Package inventory and consumption tracking.
CREATE TABLE client_test_purchases (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
package_id TEXT NOT NULL REFERENCES test_packages(id),
status TEXT CHECK(status IN ('requested', 'available', 'used')),
purchased_at TEXT DEFAULT CURRENT_TIMESTAMP,
used_at TEXT,
assigned_test_id TEXT UNIQUE REFERENCES tests(id),
custom_max_candidates INTEGER,
custom_max_questions INTEGER
);
Business Rules:
requested: Client requested, awaiting Super Admin approvalavailable: Approved, available for test creationused: Assigned to a test, consumed- One purchase can only be used for one test
Test Billing
Links tests to consumed packages with locked limits.
CREATE TABLE test_billing (
test_id TEXT PRIMARY KEY REFERENCES tests(id),
purchase_id TEXT NOT NULL REFERENCES client_test_purchases(id),
package_id TEXT NOT NULL REFERENCES test_packages(id),
max_questions INTEGER NOT NULL,
max_candidates INTEGER NOT NULL,
basic_proctoring INTEGER DEFAULT 0,
camera_proctoring INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Limits copied from package (or custom overrides) at assignment
status = 'completed'when candidate capacity exhausted- Linked test becomes
read_only = 1when completed
Support Tables
Client Limits
Manual limit overrides for clients.
CREATE TABLE client_limits (
client_id TEXT PRIMARY KEY REFERENCES clients(id),
max_exams_per_month INTEGER DEFAULT -1,
max_students_per_exam INTEGER DEFAULT -1,
max_questions_per_exam INTEGER DEFAULT -1,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Business Rules:
- Overrides subscription plan limits
-1indicates unlimited- Takes precedence over subscription limits
Client Features
Feature flag overrides for clients.
CREATE TABLE client_features (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
feature_name TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(client_id, feature_name)
);
Business Rules:
- Overrides subscription plan features
- Can enable features not in subscription plan
Client Usage Monthly
Monthly resource consumption tracking.
CREATE TABLE client_usage_monthly (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id),
month TEXT NOT NULL,
exams_created INTEGER DEFAULT 0,
attempts_created INTEGER DEFAULT 0,
storage_used_mb REAL DEFAULT 0,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(client_id, month)
);
Business Rules:
- Used for monthly quota enforcement
- Storage tracking for proctoring evidence
Global Settings
Platform-wide configuration.
CREATE TABLE global_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Default Settings: maintenance_mode: false, announcement_banner: '', registration_enabled: true, platform_logo: ''
Audit Logs
Security and compliance audit trail.
CREATE TABLE audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES profiles(id),
action TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
changes TEXT,
metadata TEXT,
ip_address TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Indexes
Important indexes for performance:
-- Core performance indexes
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_client_id ON user_roles(client_id);
CREATE INDEX idx_profiles_client_id ON profiles(client_id);
CREATE INDEX idx_questions_client_id ON questions(client_id);
CREATE INDEX idx_questions_folder_id ON questions(folder_id);
CREATE INDEX idx_tests_client_id ON tests(client_id);
CREATE INDEX idx_tests_share_code ON tests(share_code);
CREATE INDEX idx_tests_folder_id ON tests(folder_id);
CREATE INDEX idx_tests_status ON tests(status);
CREATE INDEX idx_tests_scheduled_start ON tests(scheduled_start);
CREATE INDEX idx_attempts_student_id ON attempts(student_id);
CREATE INDEX idx_attempts_test_id ON attempts(test_id);
CREATE INDEX idx_attempt_answers_attempt_id ON attempt_answers(attempt_id);
CREATE INDEX idx_test_questions_test_id ON test_questions(test_id);
CREATE INDEX idx_test_questions_section_id ON test_questions(section_id);
CREATE INDEX idx_test_questions_position ON test_questions(position);
CREATE INDEX idx_test_sections_test_id ON test_sections(test_id);
CREATE INDEX idx_test_folders_client_id ON test_folders(client_id);
CREATE INDEX idx_question_folders_client_id ON question_folders(client_id);
-- Proctoring and audit indexes
CREATE INDEX idx_proctoring_attempt ON proctoring_events(attempt_id);
CREATE INDEX idx_proctoring_test ON proctoring_events(test_id);
CREATE INDEX idx_proctoring_created ON proctoring_events(created_at);
CREATE INDEX idx_attempts_student_test ON attempts(student_id, test_id);
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_attempts_started ON attempts(started_at);
-- Subscription indexes
CREATE INDEX idx_client_subs_status ON client_subscriptions(status);
CREATE INDEX idx_client_subs_expiry ON client_subscriptions(expiry_date);
Relationships
Key entity relationships:
- Client → Profiles: One-to-many (organizations have multiple users)
- Client → Tests: One-to-many (organizations create multiple exams)
- Client → Subscriptions: One-to-one (each client has one active subscription)
- Client → Question Folders: One-to-many
- Test → Sections: One-to-many
- Test → Test Questions: One-to-many through join table
- Test → Attempts: One-to-many (exams can have multiple student attempts)
- Attempt → Attempt Answers: One-to-many
- Attempt → Proctoring Events: One-to-many
Multi-Tenancy Strategy
All main tables include client_id field ensuring:
- Complete data isolation between organizations
- Row-level security enforcement
- BOLA/IDOR protection
- Efficient multi-tenant queries
Migrations Strategy
The system uses runtime schema evolution with automatic migrations:
- Additive Only: Only adds columns/tables, never removes
- IF NOT EXISTS: Safe creation of tables and columns
- Error Tolerance: Duplicate column errors are caught and ignored
- Data Preservation: Existing data preserved during migrations
- Automatic Execution: Migrations run at server startup
Migration Example:
// Adding new column to existing table
try {
await db.execute(
`ALTER TABLE questions ADD COLUMN question_type TEXT DEFAULT 'mcq'`,
);
} catch (err: any) {
if (!err.message.includes("duplicate column")) {
console.error("Migration error:", err);
}
}
Backup & Recovery
- Turso provides automatic daily backups
- Point-in-time recovery available
- Database replication across regions
- See Monitoring & Operations for details