Stop scrambling. Start standing in.
ReliefCher is an open relief-planning platform for Singapore schools that replaces the daily WhatsApp chaos of coordinating teacher absences and relief coverage. Teachers report absences in under 30 seconds; the system instantly identifies who is free, auto-assigns relief fairly using configurable rules, and gives coordinators a single real-time dashboard to manage everything. Built for the Singapore school context — MOE timetable formats, OTPaaS authentication, Singpass identity verification, and government cloud deployment.
How this was built: ReliefCher was built using Claude Code — Anthropic's AI coding agent — with Superpowers as a prompt-engineering layer for structured reasoning. The developer is a product manager, not a software engineer. Agent output quality is governed by session rules (
CLAUDE.md), git hooks (Husky), and structured skills (TDD, debugging, code review).
- The Problem
- Our Solution
- Key Features
- Auto-Assign Algorithm
- Security and Authentication
- Tech Stack
- Deployment Architecture
- Background Workers
- Database Schema
- Application Routes and Roles
- Local Setup
- Running Tests
- Project Structure
- Piloting ReliefCher
Every school morning plays out the same way: a teacher texts the WhatsApp group at 6:30 AM — "MC today, can someone cover my classes?" What follows is a 45-minute scramble of messages, spreadsheets, and sticky notes as the relief coordinator tries to figure out who is free, who has already been assigned too many relief periods this week, and which classes are still uncovered by the time the bell rings.
Most schools track relief assignments manually or through fragmented tools that don't talk to each other. Absences are reported in one system, payroll in another, and the actual assignment happens over chat. Classes fall through the cracks. Teachers burn out from uneven workload distribution. Coordinators spend their mornings firefighting instead of supporting students.
ReliefCher gives schools a single, real-time platform to manage the full relief workflow — from the moment a teacher reports sick to the moment every period is covered.
flowchart LR
A["Teacher reports\nabsence (~30s)"] --> B["System detects\navailable staff"]
B --> C["Auto-assign\nalgorithm runs"]
C --> D["Coordinator reviews\non dashboard"]
D --> E["Notifications sent\n(Telegram / Email)"]
E --> F["Relief teacher\nacknowledges"]
Teachers report absences in under 30 seconds. The system instantly knows who is free based on the school's actual timetable. A smart auto-assign algorithm distributes relief fairly using configurable rules (workload caps, subject-match priority, consecutive-period optimisation). The relief coordinator sees everything on one dashboard and can override any assignment with a click.
No more WhatsApp threads. No more guesswork. No more uncovered classes.
Paste your aSc TimeTables CSV or XML export and the entire teacher, period, class, and subject structure is built automatically. Supports weekly, two-week (odd/even), and ten-day timetable cycles. No manual data entry — teachers, periods, classes, and subjects are created in a single transaction with deduplication.
Teachers pick their name, select the dates and affected periods, and submit. Takes ~30 seconds. The public report form at /<school-slug>/report can operate without login (legacy mode) or require MOE iCON SSO (per-school toggle). Supports 11 absence reason types including sick leave, childcare leave, compassionate leave, and official duty.
Absent teachers leave handover instructions for each period individually — not one freetext blob for the whole absence. Relief teachers see exactly what to do for each class they're covering. Coverage decisions (relief required vs. no relief needed) are set per-period, not per-absence.
The system cross-references the school timetable (including odd/even week rotations and 10-day cycles) to show exactly which teachers are free for each period. Hard constraints are always enforced: no self-cover, no double-booking, no assigning sick teachers.
Configurable rules based on workload fairness ("Hearts" system with weekly reset), subject/role priority ("Stars"), consecutive-period optimisation, and more. Two algorithm modes: cascade (rule-based filtering) and weighted (Rank-Order Centroid scoring with 2-opt swap). See Auto-Assign Algorithm for details.
A single screen showing who is absent today, which periods need coverage, and who has been assigned. Includes a timetable grid with relief overlay, staff roster with bulk CSV import, and configurable algorithm settings. Auto-refreshes with optimistic updates.
Reach beyond your school's staff to MOE's wider relief teacher pool (SRE/FAJT/CAJT). Pool teachers register via Singpass/MyInfo for identity verification. The matching engine scores candidates by proximity (postal code → URA planning area), subject match, teaching scheme, and school affiliation. Tiered matching: preferred affiliates first, then wider pool.
Relief teachers receive assignments and respond directly via Telegram bot (inline accept/decline buttons). Assignments are pushed to Google Calendar with reminders. Daily summary emails are sent to school leaders.
Superadmin dashboard with system-wide visibility: school management, user management, pool teacher administration (status, subjects, availability), algorithm tuning, audit logs, lead management, and user impersonation (dev/test only).
A seeded demo workspace at /demo/dashboard lets stakeholders explore the full dashboard without a production account. Demo session is path-scoped with its own cookie.
ReliefCher supports two algorithm modes, configurable platform-wide via SystemConfig:
Each rule narrows the candidate pool sequentially. The first rule that reduces the pool to one candidate wins.
flowchart TD
A["All available teachers"] --> B["H1: Remove absent teacher"]
B --> C["H2: Remove double-booked"]
C --> D["H3: Remove sick teachers"]
D --> E["R1: Balance Workload\n(fewest shifts this week)"]
E --> F["R2: Match Subject\n(same subject preference)"]
F --> G["R3: Prefer Same Class\n(already teaches this class)"]
G --> H["R4: Prefer Consecutive\n(same teacher for back-to-back)"]
H --> I["R5: Follow Role Priority\n(school-defined role order)"]
I --> J["R6: Avoid Back-to-Back\n(deprioritise 3+ consecutive)"]
J --> K["R7: Hard Weekly Cap\n(enforce max relief/week)"]
K --> L["Alphabetical tie-break"]
L --> M["Winner assigned"]
Rank-Order Centroid weights are derived from the admin's drag-to-reorder rule order — no manual weight tuning. Each candidate is scored across all rules simultaneously, followed by a 2-opt pairwise swap pass to catch cross-slot trades.
| ID | Constraint | Description |
|---|---|---|
| H1 | No self-cover | Absent teacher never covers their own class |
| H2 | No double-book | Cannot assign a teacher already teaching that period |
| H3 | No sick assignment | Cannot assign a teacher marked absent that day |
| ID | Rule | Status | Description |
|---|---|---|---|
| R1 | Balance Workload | Shipped | Prefer teachers with fewer relief shifts this week (resets Monday). Configurable fairness window (weekly or rolling lookback). |
| R2 | Match Subject | Shipped | Prefer teachers who teach the same subject as the absent teacher. Soft-skip if no match. |
| R3 | Prefer Same Class | Shipped | Prefer teachers who already teach the same class in another period. |
| R4 | Prefer Consecutive | Shipped | Keep the same reliever across back-to-back periods of the same class. |
| R5 | Follow Role Priority | Shipped | School-defined role ordering (e.g. SRE → untrained → FAJT → EO). |
| R6 | Avoid Back-to-Back | Shipped | Deprioritise teachers already on 3+ consecutive teaching periods that day. Configurable threshold. |
| R7 | Hard Weekly Cap | Shipped | Treat teacher as unavailable once they hit a configurable max shifts/week. |
| R8 | Longer-Horizon Fairness | Proposed | Extend fairness beyond weekly reset (4–8 week rolling history). |
| R9 | Willingness Opt-In | Proposed | Flag teachers willing to take extra relief (professional development points). |
| R10 | Multi-Day Continuity | Proposed | Reuse the reliever from day 1 of a multi-day absence on subsequent days. |
sequenceDiagram
participant U as User
participant App as ReliefCher
participant OTP as TechPass OTPaaS
participant DB as Database
U->>App: Enter email at /login
App->>OTP: POST /otp/send (HMAC-signed API key)
OTP-->>U: 6-digit OTP via email
U->>App: Submit OTP
App->>OTP: POST /otp/verify
OTP-->>App: Valid / Invalid
App->>DB: createSession(userId, token, expiresAt)
App-->>U: Set HTTP-only cookie (reliefcher_session)
| Method | Purpose | Gate |
|---|---|---|
| TechPass OTPaaS | Primary login for all users | Email must be allowlisted in OTPaaS |
| Google OAuth (MOE iCON) | SSO for MOE staff | Server-side restricted to @moe.edu.sg; requires email_verified === true |
| Singpass/MyInfo | Pool teacher identity verification | Fetches NRIC, name, DOB, address from MyInfo API |
Custom stateful session system — not NextAuth. A 32-byte random token is stored in an HTTP-only cookie (reliefcher_session) and looked up against the Session table on each request. Sessions expire after 7 days. Instant revocation: delete the row and the user is logged out everywhere. This was chosen over JWTs because the OTPaaS integration is a single-provider flow that doesn't benefit from NextAuth's multi-provider abstractions, and stateful sessions give us the revocation guarantee.
| Control | Implementation |
|---|---|
| NRIC encryption | AES-256-GCM (src/lib/encryption.ts); separate HMAC-SHA256 hash for lookups (prevents rainbow tables) |
| Secret management | All credentials via environment variables; .env files gitignored; .env.example committed with placeholders |
| Input validation | Zod schemas at API boundary; Prisma parameterised queries (no raw SQL) |
| Audit logging | Append-only AuditLog and AbsenceReportAudit tables; tracks auth attempts, admin mutations, pool operations |
| Transport security | sslmode=verify-ca with AWS RDS CA bundle baked into Docker image |
| Impersonation | Gated by ENABLE_IMPERSONATION env flag; dev/test only; never in production |
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Server Components) |
| Language | TypeScript 5, React 19 |
| ORM | Prisma 7 + PrismaPg driver adapter |
| Database | PostgreSQL 15 |
| Auth | TechPass OTPaaS + Google OAuth (MOE iCON) + Singpass/MyInfo |
| Session | Custom stateful (HTTP-only cookie + DB lookup) |
| Styling | Tailwind CSS 4 + Flow Design System (@flow/core) + Radix UI Colors |
| Icons | Lucide React |
| Animations | Framer Motion |
| Validation | Zod 4 |
| Job queue | Redis + BullMQ (ioredis) |
| Telegram | grammy |
| Resend | |
| Unit / integration tests | Vitest 4 + Testing Library |
| End-to-end tests | Playwright |
| CI / hooks | Husky (pre-push: unit tests) |
| Containerisation | Docker (multi-stage, Node 22 Alpine) |
| Deployment target | AWS GCC (ECS/EC2 + ALB + RDS) |
flowchart LR
subgraph Local
L_PG["Postgres :5432"]
L_RD["Redis :6379"]
L_APP["next dev :3000"]
L_WK["npm run workers"]
end
subgraph "Test (Vercel — interim)"
T_APP["Vercel Edge"]
T_PG["Supabase Postgres"]
end
subgraph "Prod (AWS GCC)"
P_ALB["ALB"]
P_ECS1["ECS: Next.js app"]
P_ECS2["ECS: worker-matching"]
P_ECS3["ECS: worker-notification"]
P_ECS4["ECS: worker-calendar"]
P_RDS["RDS PostgreSQL"]
P_RD["ElastiCache Redis"]
P_OTP["TechPass OTPaaS"]
P_ALB --> P_ECS1
P_ECS1 --> P_RDS
P_ECS1 --> P_RD
P_ECS2 --> P_RDS
P_ECS2 --> P_RD
P_ECS3 --> P_RD
P_ECS4 --> P_RD
P_ECS1 --> P_OTP
end
| Environment | Hosting | Database | Workers | Auth |
|---|---|---|---|---|
| Local | localhost:3000 |
Docker Compose Postgres (port 5432) | npm run workers (all 3 queues in one process) |
OTPaaS sandbox + Google/Singpass sandbox |
| Test (interim) | Vercel | Supabase Postgres | Not required (manual testing) | OTPaaS test + Google/Singpass test |
| Prod | AWS GCC (ECS/EC2 + ALB) | AWS RDS PostgreSQL | 3 ECS tasks (one per queue) | OTPaaS production + Google/Singpass production |
The Dockerfile uses three stages:
- deps —
npm ci, copy Prisma schema - builder —
prisma generate,next build - runner — Alpine runtime with compiled
.nextoutput, worker sources (run viatsxat runtime), and AWS RDS CA bundle for TLS verification
A single Docker image serves both the web app and workers. The role is selected at runtime via the WORKER_QUEUE environment variable.
flowchart LR
API["Next.js API Routes"] -->|enqueue| RD["Redis"]
RD --> W1["Worker: matching"]
RD --> W2["Worker: notification"]
RD --> W3["Worker: calendar"]
W1 -->|"PoolAssignment\ncreation"| DB[(Postgres)]
W2 -->|"Telegram / Email\ndelivery"| EXT["grammy / Resend"]
W3 -->|"Event create\n/ update"| GCAL["Google Calendar API"]
| Queue | Responsibility | Triggers |
|---|---|---|
| matching | Pool teacher candidate ranking, assignment creation, offer expiry | Absence report submitted; daily 5:30am cron |
| notification | Email (Resend) and Telegram (grammy) delivery with retry | Assignment created; pool offer sent; daily summary |
| calendar | Google Calendar event create/update/delete with VALARM reminders | Pool assignment confirmed; assignment changed |
- Local dev:
npm run workersruns all three queues in a single Node process (WORKER_QUEUE=all) - Production (GCC): Three separate ECS tasks, each with
WORKER_QUEUE=matching|notification|calendar - Notification outbox: All sends are persisted in the
OutboundMessagetable before queue processing. Status lifecycle:PENDING→SENT|FAILED|CANCELLED. Exponential backoff with configurable max retries.
erDiagram
School ||--o{ Teacher : has
School ||--o{ Period : has
School ||--o{ TimetableEntry : has
School ||--o{ AbsenceReport : has
School ||--o{ ReliefAssignment : has
School ||--o{ SchoolRole : has
School ||--o{ Position : has
School ||--o| MatchingConfig : has
Teacher ||--o{ TimetableEntry : teaches
Teacher ||--o{ AbsenceReport : reports
Teacher ||--o{ ReliefAssignment : "covers for"
AbsenceReport ||--o{ ReliefAssignment : generates
AbsenceReport ||--o{ AbsencePeriodRequirement : "has per-period"
AbsenceReport ||--o{ PoolAssignment : "triggers"
TimetableEntry }o--|| Period : "in period"
PoolTeacher ||--o{ PoolTeacherSubject : teaches
PoolTeacher ||--o{ PoolTeacherLevel : "qualified for"
PoolTeacher ||--o{ SchoolAffiliation : "affiliated with"
PoolTeacher ||--o{ PoolAssignment : receives
User ||--o{ Session : has
| Model | Purpose |
|---|---|
School |
Multi-tenant root. Holds timetable cycle config, algo rules JSON, postal code for proximity matching. |
User |
App users with roles: SUPERADMIN, SCHOOL_ADMIN, TEACHER. Email-based auth via OTPaaS/Google. |
Session |
Stateful auth tokens. HTTP-only cookie, 7-day expiry, instant revocation. |
Teacher |
School staff member. Linked to timetable entries, subjects, school role, and position. |
SchoolRole |
Configurable roles (e.g. "Relief Teacher", "FAJT", "EO") with priority order for auto-assign. Category flag: TEACHER, PERMANENT_RELIEF, EXTERNAL_RT. |
Position |
Job titles (e.g. "HOD English") with optional default max relief per week. |
Period |
School periods with start/end times (e.g. Period 1, 07:30–08:30). |
TimetableEntry |
The core schedule: teacher × day × period × class × subject × week type. Composite unique index. |
AbsenceReport |
Teacher absence record with reason, date range, coverage type, and per-period lesson notes. |
AbsencePeriodRequirement |
Per-period granularity: coverage decision (relief required / no relief) and lesson note for each period of an absence. |
ReliefAssignment |
Links an absence to a relief teacher for a specific timetable entry and date. Tracks acknowledgement and who assigned. |
PoolTeacher |
External relief teachers (SRE/FAJT/CAJT). NRIC encrypted (AES-256-GCM), Singpass-verified, Telegram-linked. |
PoolAssignment |
Pool teacher assignment with status lifecycle: CONFIRMED → COMPLETED |
MatchingConfig |
Per-school or platform-wide config for pool matching criteria, candidate window, cross-school conflict mode. |
OutboundMessage |
Notification outbox. Tracks every Telegram/email send with status, retry count, and provider message ID. |
AuditLog |
Append-only general audit trail for security and compliance events. |
SystemConfig |
Platform-wide settings. Currently holds algoMode (CASCADE or WEIGHTED). |
InterestLead |
Inbound interest signups from the public /interest form. CRM-lite with status tracking. |
flowchart TD
subgraph "Public (no auth)"
P1["/ — Landing page"]
P2["/interest — Early access signup"]
P3["/feedback — Feedback form"]
P4["/login — OTPaaS + Google SSO"]
P5["/<slug>/report — Absence form"]
P6["/register/pool-teacher — Singpass registration"]
end
subgraph "School Admin"
S1["/<slug>/dashboard — Relief overview"]
S2["/<slug>/dashboard/staff — Teacher roster"]
S3["/<slug>/dashboard/timetable — Timetable grid"]
S4["/<slug>/dashboard/relief-teachers — Pool teachers"]
S5["/<slug>/dashboard/settings — School config"]
end
subgraph "Pool Teacher"
PT1["/pool/profile — Profile & preferences"]
end
subgraph "Superadmin"
A1["/admin/dashboard — System overview"]
A2["/admin/schools — School management"]
A3["/admin/users — User management"]
A4["/admin/pool-teachers — Pool teacher admin"]
A5["/admin/algorithm — Algorithm tuning"]
A6["/admin/audit-logs — Audit trail"]
A7["/admin/leads — Interest leads"]
A8["/admin/impersonate — User impersonation"]
end
| Route | Access | Description |
|---|---|---|
/ |
Public | Landing page with features, problem/solution narrative |
/login |
Public | OTPaaS email OTP + Google OAuth (MOE iCON) entry point |
/interest |
Public | Early-access signup form for schools |
/feedback |
Public | Feedback form (creates GitHub issues) |
/[slug]/report |
Public or SSO | Absence reporting form. Per-school toggle for SSO requirement. |
/[slug]/reporting |
Public | Post-submission confirmation page |
/[slug]/dashboard |
School Admin | Main relief dashboard: today's absences, assignments, coverage status |
/[slug]/dashboard/staff |
School Admin | Teacher roster with CSV import, bulk operations, subject management |
/[slug]/dashboard/timetable |
School Admin | Timetable grid view with relief overlay |
/[slug]/dashboard/relief-teachers |
School Admin | Pool teacher matching, preferred teachers, REMS candidates |
/[slug]/dashboard/settings |
School Admin | Algorithm rules config, matching criteria, school postal code |
/register/pool-teacher |
Public | Singpass/MyInfo registration flow for relief teachers |
/pool/profile |
Pool Teacher | Relief teacher profile, subject preferences, availability window |
/teacher/absences |
Teacher | Personal absence history |
/onboarding |
Authenticated | School setup wizard (first-time admin) |
/admin/dashboard |
Superadmin | System health, platform-wide stats |
/admin/schools |
Superadmin | School CRUD, bulk operations |
/admin/users |
Superadmin | User management, role assignment |
/admin/pool-teachers |
Superadmin | Pool teacher admin: status management, subjects, REMS import |
/admin/algorithm |
Superadmin | Algorithm mode toggle (cascade/weighted), rule visualisation |
/admin/audit-logs |
Superadmin | System audit trail |
/admin/leads |
Superadmin | Interest lead CRM (status, notes, conversion tracking) |
/admin/impersonate |
Superadmin | User impersonation for testing (dev/test env only) |
- Node.js 22+ (LTS)
- Docker and Docker Compose (for Postgres and Redis)
- npm (comes with Node.js)
1. Clone the repository
git clone https://github.com/String-sg/relief-teacher-planning.git
cd relief-teacher-planning2. Install dependencies
npm installThis also runs prisma generate via the postinstall hook.
3. Start local services
docker compose -f docker-compose.local.yml up -dThis starts Postgres on port 5432 and Redis on port 6379.
4. Configure environment
cp .env.example .env.localEdit .env.local with your values. At minimum for local development:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/reliefcher
DIRECT_URL=postgresql://postgres:postgres@localhost:5432/reliefcher
NEXTAUTH_SECRET=<any-random-32-char-string>
NEXTAUTH_URL=http://localhost:3000
REDIS_URL=redis://localhost:6379
NRIC_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000For full OTPaaS login, you also need OTPAAS_BASE_URL, OTPAAS_NAMESPACE, OTPAAS_APP_ID, and OTPAAS_SECRET. See .env.example for all variables.
5. Run database migrations
npx prisma migrate deploy6. Seed the database
npm run seedThis populates demo schools, teachers, timetables, and absence reports.
7. Start the dev server
npm run devThe app is now available at http://localhost:3000.
8. (Optional) Start background workers
npm run workersThis runs all three BullMQ worker queues (matching, notification, calendar) in a single process.
flowchart TD
A["npm test"] -->|"Fast, no DB"| B["Unit / Logic Tests\nsrc/__tests__/**/*.test.ts"]
C["npm run test:api"] -->|"Requires Postgres :5433"| D["API / Integration Tests\ntests/api/**/*.test.ts"]
E["npm run test:e2e"] -->|"Requires running app"| F["E2E / Visual Regression\ne2e/**/*.spec.ts"]
| Suite | Location | Command | Database? | Pre-push? |
|---|---|---|---|---|
| Unit / logic | src/__tests__/**/*.test.ts |
npm test |
No | Yes (Husky) |
| API / integration | tests/api/**/*.test.ts |
npm run test:api |
Yes (Postgres on :5433) | No |
| E2E / visual | e2e/**/*.spec.ts |
npm run test:e2e |
Yes (full app running) | No |
Fast, database-free tests for business logic — auto-assign rules, matching criteria, timetable import parsing, encryption, rate limiting. Runs on every git push via Husky pre-push hook. Cannot push if tests fail.
npm test # single run
npm run test:watch # watch mode
npm run test:coverage # with coverage reportFull API route tests against a real Postgres instance on port 5433. Global setup creates the test database and runs prisma migrate deploy. Tests use factory helpers (createSchool, createUser, createTeacher, etc.) and truncateAll() for cleanup between tests.
# Start test database (if not using docker-compose.local.yml)
docker run -d --name reliefcher-test-db -p 5433:5432 \
-e POSTGRES_DB=reliefcher_test -e POSTGRES_PASSWORD=postgres postgres:15
npm run test:apiPlaywright tests with screenshot comparisons on three iPhone models (SE, 14, 14 Pro Max). Requires the built app running on port 3001.
npm run build
npm run start -- -p 3001 &
npm run test:e2e # run tests
npm run test:e2e:ui # interactive UI mode
npm run test:e2e:update # update baseline snapshotsrelief-teacher-planning/
├── prisma/
│ ├── schema.prisma # Data model (30+ models, 15+ enums)
│ ├── seed.ts # Demo data seeding
│ ├── migrations/ # Migration chain (rebaselined Apr 2026)
│ └── README.md # Migration deploy/resolve guide
├── src/
│ ├── app/
│ │ ├── [slug]/ # School-scoped routes
│ │ │ ├── dashboard/ # Relief coordinator dashboard
│ │ │ │ ├── staff/ # Teacher roster & CSV import
│ │ │ │ ├── timetable/ # Timetable grid with relief overlay
│ │ │ │ ├── relief-teachers/ # Pool teacher matching
│ │ │ │ └── settings/ # School config & algo rules
│ │ │ ├── report/ # Public absence form
│ │ │ └── reporting/ # Post-submission confirmation
│ │ ├── admin/ # Superadmin panel
│ │ │ ├── dashboard/ # System overview
│ │ │ ├── schools/ # School management
│ │ │ ├── users/ # User management
│ │ │ ├── pool-teachers/ # Pool teacher admin
│ │ │ ├── algorithm/ # Algorithm tuning
│ │ │ ├── audit-logs/ # Audit trail
│ │ │ └── leads/ # Interest lead CRM
│ │ ├── api/ # API routes
│ │ │ ├── auth/ # OTPaaS, Google, Singpass flows
│ │ │ ├── absence-reports/ # Absence CRUD
│ │ │ ├── auto-assign/ # Algorithm trigger
│ │ │ ├── dashboard/ # Dashboard data + availability
│ │ │ ├── relief-assignments/ # Assignment CRUD
│ │ │ ├── pool-teachers/ # Pool teacher CRUD + import + invites
│ │ │ ├── teachers/ # Teacher CRUD + CSV import/export
│ │ │ ├── timetable/ # Timetable CRUD
│ │ │ ├── notifications/ # Outbound message management
│ │ │ ├── telegram/ # Telegram webhook
│ │ │ └── cron/ # Scheduled jobs (daily summary, cleanup)
│ │ ├── login/ # Auth entry point
│ │ ├── onboarding/ # School setup wizard
│ │ ├── register/pool-teacher/ # Singpass registration
│ │ ├── pool/profile/ # Pool teacher self-service
│ │ ├── teacher/absences/ # Teacher absence history
│ │ ├── interest/ # Early access signup
│ │ ├── feedback/ # Feedback form
│ │ └── page.tsx # Landing page
│ ├── components/
│ │ ├── landing/ # Hero, features, problem/solution sections
│ │ ├── matching/ # Pool teacher matching UI
│ │ ├── teachers/ # Teacher roster, bulk import
│ │ ├── timetable/ # Grid views, relief overlay, period strips
│ │ └── ui/ # Shared primitives (Combobox, Select, etc.)
│ ├── hooks/ # React hooks (auto-refresh, etc.)
│ ├── lib/
│ │ ├── auth.ts # Session create/validate/destroy
│ │ ├── otpaas.ts # OTPaaS HMAC key derivation
│ │ ├── singpass.ts # Singpass OAuth + MyInfo fetch
│ │ ├── autoAssign.ts # Auto-assign orchestrator
│ │ ├── autoAssignRules.ts # Rule registry (7 shipped rules)
│ │ ├── poolMatcher.ts # Pool teacher candidate ranking
│ │ ├── queue.ts # BullMQ job enqueue helpers
│ │ ├── notifier.ts # Notification routing
│ │ ├── telegram.ts # Telegram bot client (grammy)
│ │ ├── timetableImport.ts # CSV/XML → timetable pipeline
│ │ ├── parseAscXml.ts # aSc XML parser
│ │ ├── prisma.ts # Prisma client (PrismaPg adapter)
│ │ ├── encryption.ts # AES-256-GCM for NRIC storage
│ │ ├── planningAreas.ts # Postal code → URA planning area
│ │ ├── constants/ # Feature flags, role mappings
│ │ ├── dto/ # Data transfer objects
│ │ ├── scheduling/ # Cron-driven background tasks
│ │ ├── timetable/ # Timetable utilities (week type, availability)
│ │ └── types/ # TypeScript type definitions
│ ├── workers/
│ │ ├── entry.ts # BullMQ worker init (3 queues)
│ │ └── handlers/
│ │ ├── matching.ts # Pool teacher matching processor
│ │ ├── notification.ts # Email/Telegram delivery processor
│ │ └── calendar.ts # Google Calendar sync processor
│ └── __tests__/ # Unit tests (25+ test files)
├── tests/
│ └── api/ # API/integration tests
│ ├── setup.ts # Global setup (test DB creation)
│ └── helpers.ts # Factory functions + test utilities
├── e2e/ # Playwright E2E tests
│ ├── __screenshots__/ # Baseline screenshots (3 iPhone models)
│ └── fixtures/ # Playwright fixtures
├── docs/
│ ├── plans/ # Implementation plans
│ ├── research/ # User interviews, competitive analysis
│ └── superpowers/ # Feature specs and design docs
├── .env.example # Environment variable template
├── docker-compose.local.yml # Local dev (Postgres + Redis)
├── docker-compose.yml # Full stack (app + workers + migrate + seed)
├── Dockerfile # Multi-stage build
├── CLAUDE.md # Agent session rules and project conventions
├── vitest.config.ts # Unit test config
├── vitest.api.config.ts # API test config
├── playwright.config.ts # E2E test config
├── next.config.ts # Next.js configuration
└── package.json # Dependencies and scripts
We are looking for Singapore schools to pilot ReliefCher. If you are a teacher, relief coordinator, HOD, or school leader and want to try a better way to manage relief coverage, we would love to hear from you.
Or reach out to the DXD team directly — we will walk you through the platform and get your school set up.
This project is maintained by DXD. All rights reserved.