Last updated: 2026-03-05
| Priority |
Category |
Issues |
Status |
| P0 |
Critical bottlenecks |
4 |
✅ All fixed |
| P1 |
Performance at 100+ teachers |
6 |
✅ All fixed |
| P2 |
Future optimization |
5 |
Documented |
Current safe scale: ~50 teachers / 2,500 students
Target after fixes: 500+ teachers / 25,000+ students
- Multi-tenancy with
teacherId scoping on all tables
- Circuit breakers + Redis-backed rate limiting for Google/Stripe APIs
- Credit ledger idempotency via
INSERT ON CONFLICT
- Webhook idempotency (atomic dedup)
- Soft-delete pattern with
notDeleted() helper (7 tables)
- Route lazy loading (all frontend routes)
- Optimistic mutations for low-risk actions
- BullMQ job queues with retry/backoff
| Table |
Index Added |
Query Pattern |
contact_log |
(teacher_id, student_id, sent_at DESC) |
Student list last-contact aggregation |
contact_log |
(teacher_id, sent_at DESC) |
Teacher contact history |
availability_rules |
(teacher_id, is_active) WHERE is_active = true |
Slot generation on every booking |
sessions |
(teacher_id, status, starts_at, ends_at) |
Conflict detection for bookings |
| 7 soft-delete tables |
(teacher_id) WHERE deleted_at IS NULL |
Partial indexes for active records |
Migration: 0074_add_performance_indexes.sql — 11 indexes total. Schema files updated for contact_log, availability_rules, class_sessions.
|
|
| Files |
slot-engine.ts, session-service.ts |
| Fix |
isSlotAvailable() uses SELECT ... FOR UPDATE SKIP LOCKED when called inside a transaction. 6 session-creation methods now wrap availability check + insert in db.transaction(): bookSession, bookMultipleSessions, createTrialSession, scheduleManualSession, rescheduleSession, createRecurringSessions. |
|
|
| File |
slot-engine.ts |
| Fix |
MAX_SLOT_RANGE_DAYS = 14 — date range capped at top of getAvailableSlots(). Prevents 60K+ array elements in memory. |
|
|
| File |
packages/db/src/index.ts |
| Fix |
postgres(connectionString, { max: 20, idle_timeout: 30, connect_timeout: 5 }) |
| File |
Method |
Fix |
review-service.ts |
getStats() |
Replaced in-memory aggregation with COUNT(*), AVG(rating), GROUP BY rating SQL |
legal-document-service.ts |
create() |
Replaced findMany + reduce with SELECT MAX(sort_order) SQL aggregate |
| Worker |
Pattern |
Fix |
lifecycle-detection-worker |
Was: sequential loop, concurrency: 1 |
Dispatcher + per-teacher jobs, concurrency: 5 |
content-analytics-worker |
Was: sequential loop, concurrency: 1 |
Dispatcher + per-teacher jobs, concurrency: 5 |
metric-alerts-worker |
Was: sequential loop, concurrency: 1 |
Dispatcher + per-teacher jobs, concurrency: 5 |
Pattern: Cron triggers a dispatcher job (no teacherId) that queries all teachers and enqueues one job per teacher on the same queue. Per-teacher jobs run in parallel with concurrency: 5. Date-based jobId (worker-{teacherId}-{YYYY-MM-DD}) provides daily idempotency. Individual teacher errors throw (proper BullMQ failure tracking) instead of silently continuing.
|
|
| Fix |
Added staleTime: 2min to dashboard summary, smart actions, pending sessions, and calendar queries. Settings query cached for 5 min. |
Backend Redis cache remains a future optimization.
| Route |
Query |
staleTime Added |
| Students |
teacher-students |
2 min |
| Messages |
teacher-students-messages |
2 min |
| Payments |
teacher-payments, teacher-payments-kpis |
1 min |
| Dashboard |
teacher-dashboard-summary, teacher-dashboard-smart-actions, teacher-sessions-pending, teacher-calendar |
2 min |
| Dashboard |
teacher-settings |
5 min |
| Payments |
teacher-settings |
5 min |
| Organization |
tags |
5 min |
Total: 11 useQuery hooks updated across 5 route files.
|
|
| Files |
session-service.ts, calendar-sync-worker.ts |
| Fix |
queueCalendarSync() marks syncStatus='pending' in session_calendar_sync and enqueues a BullMQ job. Calendar-sync worker (5-min cron) processes pending records in batches of 20. Session creation never blocks on Google Calendar API. Circuit breaker skips batch when google-calendar circuit is open. |
10 call sites in session-service.ts use queueCalendarSync() for create/update/delete operations.
|
|
| Files |
session-service.ts, lib/redis.ts |
| Fix |
Cache-aside pattern: getSession() checks Redis (session:{id} key, 30s TTL) → falls back to PostgreSQL on miss → populates cache on hit. deleteSession() invalidates cache before DB delete. All Redis operations wrapped in try/catch — auth falls back to direct DB queries if Redis is unavailable. Shared ioredis singleton (lib/redis.ts) with lazy connect and exponential backoff. |
| # |
Issue |
Notes |
| P2-1 |
No list virtualization (students, contacts, payments) |
Add @tanstack/react-virtual when lists exceed 100 items |
| P2-2 |
i18n: 5,700 lines in single file, both languages loaded |
Split into namespace files, lazy-load secondary language |
| P2-3 |
Analytics queries without materialized views |
Create retention_cohorts materialized view, refresh hourly |
| P2-4 |
No BullMQ queue depth monitoring |
Add Prometheus metrics + alerts on depth > threshold |
| P2-5 |
Teachers table: 38KB avg JSONB per row |
Ensure all queries use column selection, not SELECT * |
| Metric |
Current (~10) |
100 teachers |
1,000 teachers |
| Students |
~500 |
5K |
50K |
| Sessions |
~5K |
50K |
500K |
| Contact logs |
~2K |
20K |
200K |
| Auth queries/sec |
~10 |
~100 |
~1,000 → Redis cached (P1-6 ✅) |
| Slot generation |
10ms |
30ms |
100ms (was 500ms) |
| Lifecycle job |
10s |
100s |
~34min with concurrency:5 (P1-2 ✅) |