Overview
Athena uses a shared database, shared schema architecture where multiple schools (tenants) share the same PostgreSQL database and tables. Tenant isolation is enforced through:- Database: Every table has a
school_idcolumn - Backend: Middleware automatically injects
school_idinto queries - Frontend: Users belong to one or more schools via
school_memberships
Why This Approach?
Shared Schema Architecture
✅ Chosen: Shared Schema
- Single codebase
- Easy cross-tenant analytics (superadmin)
- Simple backup/restore
- Lower operational overhead
- Cost-effective for 20-40 schools
❌ Not Using: Schema-per-Tenant
- Complex migration management
- Harder to aggregate data
- Higher operational cost
- Only needed at massive scale (1000+ tenants)
Backend Enforcement (No RLS)
✅ Why Backend Enforcement
✅ Why Backend Enforcement
TestableDebuggable
- Stack traces show where tenant check happens
- No hidden database policies
- Logs show tenant context
- Works on any PostgreSQL
- No vendor lock-in to Supabase/RLS
- Easy to migrate to GCP Cloud SQL
- All business rules in Python
- No split between DB and app
- Version controlled with Git
❌ Why Not RLS
❌ Why Not RLS
Hard to Test
- Policies run inside PostgreSQL
- Harder to mock for unit tests
- Must set
app.settings.jwt.claims.tenant_idin every test query
- Silent failures (empty results instead of errors)
- No Python stack trace
- Must check PostgreSQL logs
- RLS syntax varies between vendors
- Migration requires rewriting policies
- Not all managed services support RLS fully
- Permissions in both database and backend
- Harder to understand complete flow
- Two places to update when rules change
Trade-off Accepted: A bug in backend middleware could leak data. This is mitigated by:
- Comprehensive integration tests for tenant isolation
- Code reviews focused on tenant checks
- Middleware that makes forgetting the filter the exception, not the rule
How It Works
1. Database: Every Table Has school_id
2. Backend: Automatic Tenant Injection
Middleware Flow
Implementation: deps.py
3. Frontend: School Selection
Single-School Users
Multi-School Users (Superadmin)
The frontend Axios client automatically injects
X-School-Id from Zustand auth store for all requests.Roles & Permissions
Role Definitions
Superadmin Special Case
Testing Tenant Isolation
Integration Test Pattern
Test Coverage Checklist
Cross-Tenant Write
User from school A tries to create data with
school_id=school_b
→ Should get 403 or ignore the fieldCross-Tenant Update
User from school A tries to update school B record by ID
→ Should get 404 (filtered query returns nothing)
Performance Considerations
Index Strategy
❌ Bad: Single-column index
✅ Good: Composite index
Query Patterns
Security Checklist
✅ Backend
✅ Backend
- Every endpoint depends on
get_current_tenantorget_auth_context - Every query includes
WHERE school_id = tenant.id - Superadmin bypass explicitly checked with
has_permission("manage:schools") - Foreign key references validated within same tenant
- Integration tests verify 403 on cross-tenant access
✅ Database
✅ Database
- Every table (except
schools) hasschool_idcolumn - Foreign keys include
school_idfor composite uniqueness - Indexes always start with
school_id - No direct SQL queries bypass ORM (unless explicitly tenant-filtered)
✅ Frontend
✅ Frontend
- Axios interceptor always sends
X-School-Idheader - Multi-school users explicitly select active school
- School context displayed in UI (navbar, breadcrumbs)
- No hardcoded school IDs in code
Migration Path
If you later need schema-per-tenant:This architecture makes the migration path optional and gradual, not required upfront.
Common Pitfalls
🚨 Forgetting school_id in WHERE clause
🚨 Forgetting school_id in WHERE clause
Problem:Solution:
🚨 Validating foreign keys without tenant check
🚨 Validating foreign keys without tenant check
Problem:Solution:
🚨 Missing X-School-Id header
🚨 Missing X-School-Id header
Problem:
Multi-school user doesn’t send header, gets ambiguous error.Solution:
Diagram: Request Flow
Next Steps
Technology Stack
Back to the complete tech stack overview
API Reference
See how these patterns apply to specific endpoints