Backend service for the Verboheit Mathematics League Competition, a free annual mathematics competition for senior secondary school students (SS1–SS3) across Nigeria.
The backend powers the full competition lifecycle: candidate registration and identity verification, multi-stage exam delivery, automated scoring, cumulative leaderboards, real-time proctoring, helpdesk, and staff administration.
This project -- including the UIs, vmlc-frontend and vmlc-landing -- was built to address a need for better administrative workflows and a more capable digital environment for running the competition, identified and led by @theolujay.
- Competition Stages
- Tech Stack
- Architecture
- Project Structure
- Quick Start
- Configuration
- API Documentation
- Deployment
- Contributing
| Stage | Format | Advancement |
|---|---|---|
| Screening | Virtual Test | A select number of top performers advance |
| League | Six rounds of virtual exams, one per week | Cumulative standings; top selction advance |
| Final | In-person exam | Knockout round among top qualifiers. Top three win |
Staff members determine the questions and exam constraints.
| Layer | Technology |
|---|---|
| Framework | Django 5.2, Django REST Framework 3.16 |
| ASGI Server | Uvicorn + Gunicorn |
| Real-time | Django Channels 4, Redis channel layer |
| Background Jobs | Celery 5.5, Celery Beat, Redis broker |
| Database | PostgreSQL 17 |
| Cache / Broker | Redis 7 |
| Storage | AWS S3 via django-storages (public & private buckets) |
| Auth | JWT (SimpleJWT), API keys, session fallback |
| Observability | OpenTelemetry, Prometheus, Grafana, structured JSON logging |
| Monitoring | django-health-check, django-prometheus |
The application is fully ASGI. HTTP and WebSocket traffic share the same process and port, routed by a ProtocolTypeRouter in config/asgi.py.
Incoming HTTP requests are handled by Uvicorn (behind Gunicorn) and dispatched to Django REST Framework views. WebSocket connections are handled by Django Channels consumers. Both share the same ASGI entrypoint.
The codebase is split into four Django apps, each with a distinct responsibility:
identity-- User accounts, candidate and staff profiles, registration, and authentication and authorization.vmlc-- The core exam engine: question delivery, candidate sessions, automated scoring, violation tracking, and WebSocket-based proctoring.competition-- Competition and stage lifecycle management; enrollments, stage promotions, ranking snapshots, and leaderboard publishing.comms-- Real-time communications: WebSocket consumers for notifications and helpdesk, Celery-backed broadcast dispatch, email, and SMS.
Heavy operations are offloaded to Celery workers across named queues. This includes exam scoring, ranking computation, notification dispatch, cache warming, and file processing. Celery Beat handles scheduled jobs such as periodic cache updates and results publishing.
PostgreSQL is the primary data store. Redis serves dual duty as the Celery broker and the Django Channels channel layer. Static and user-uploaded files are stored in AWS S3, with separate public and private buckets managed by custom storage backends.
config/ Django project bootstrap
├── settings/ Environment-specific settings (dev, prod, staging, docker_dev)
├── asgi.py ASGI application and WebSocket routing
├── celery.py Celery app and beat schedule
└── urls.py API route registration
identity/ User and authentication
├── models.py User, Candidate, Staff, EmailOTP
├── permissions.py Role hierarchy (superadmin → volunteer)
└── views.py Registration, login, verification, profile
vmlc/ Core exam engine
├── models.py Exam, Question, CandidateExamResult, Heartbeat, ViolationEvent
├── serializers/ DRF serializers
├── views.py Exam-taking, scoring, leaderboards
├── consumers.py WebSocket consumers for proctoring
├── tasks.py Celery tasks (scoring, ranking, notifications)
└── storage_backends.py S3 public/private storage backends
competition/ Competition lifecycle
├── models.py Competition, Stage, StageExam, Enrollment, RankingSnapshot
├── views.py Dashboards, rankings, leaderboards, stage promotions
└── tasks.py Celery tasks (cache updates, notifications)
comms/ Communications and real-time
├── consumers.py Unified WebSocket consumer (notifications, helpdesk, broadcasts)
├── middleware.py DualAuthMiddlewareStack (JWT / session / API key)
├── models.py HelpdeskThread, Broadcast, Notification, BackupLog
├── tasks.py Celery tasks (broadcast dispatch, email, SMS)
└── routing.py WebSocket URL patterns
scripts/ Container lifecycle entrypoints
docker/ Docker support files
infra/ Infrastructure and deployment manifests
- Python 3.13
- PostgreSQL 17
- Redis 7
cp .example.env .env
docker compose up --buildMigrations run automatically via the db-migrate service. The Django server, Celery worker, and Celery Beat all start once migrations complete.
The API is available at http://localhost:8000. Postman Collection here.
cp .example.env .env
uv sync --group dev
python manage.py migrate
python manage.py runservermake test
# OR
make test/docker # if running with Docker ComposeSettings are selected via the DJANGO_SETTINGS_MODULE environment variable:
| Environment | Settings Module | Config File |
|---|---|---|
| Local dev | config.settings.dev |
.env |
| Docker dev | config.settings.docker_dev |
.env |
| Test | config.settings.test |
.env |
| Staging | config.settings.staging |
Docker Secrets |
| Production | config.settings.prod |
Docker Secrets |
See .example.env for all available variables and their defaults.
CI/CD is managed via GitHub Actions (.github/workflows/). Two pipelines are active:
| Branch | Environment | Steps |
|---|---|---|
release / preview |
Staging | Build → test → push to GHCR → deploy → health check → Slack notification |
main |
Production | Build → test → push to GHCR → deploy → health check → Slack notification |
Branch from main, make your changes, ensure all tests pass, then open a pull request against release.
# Branch naming
git checkout -b feat/your-feature-name
git checkout -b fix/short-descriptionBefore submitting:
make test
make format
make deadcodeBuilt for Verboheit · verboheit.org
If you'd like to be a volunteer, please register here or send an email