Skip to content

Repository files navigation

Planify — University Course Timetabling Platform

A production-grade, multi-tenant web platform that turns course/room/teacher data into conflict-free timetables — powered by an ML-enhanced Tabu Search solver, shipped as containers, hardened for the real world, and built test-first.

CI Release images Tests Docker GHCR Backend Frontend License


Planify — landing page

What it is

Planify lets an institution's administrators enter their catalogue (rooms, groups, teachers, modules) and courses, then generate an optimal weekly timetable at the click of a button. The heavy optimization runs asynchronously in a dedicated solver service (a separate research project, ml-tabu-uctp — ML-Enhanced Tabu Search), consumed as a black-box HTTP service. Results are rendered as an interactive, filterable grid with (H, S) quality indicators and one-click PDF/CSV export.

It is multi-tenant (each establishment is isolated), invitation-only, and role-aware (super_admin · admin · teacher).

Login Dashboard Generated timetable
Login Dashboard Timetable

Architecture (Docker)

Docker architecture

Eight containers orchestrated by Docker Compose, on a private network where only the reverse proxy is exposed:

Layer Container Role
Edge proxy (Caddy) TLS-terminated by Coolify/Traefik in prod; routes / → frontend, /api → backend; injects security headers
UI frontend (Next.js 15 + shadcn/ui) Auth, catalogue, timetable editor, grid + exports
API backend (FastAPI) Auth/RBAC, CRUD, validation, enqueues solve jobs — never blocks on the solver
Queue redis Async job queue for long-running solves
Compute worker ×2 (httpx client) Pulls jobs, calls the solver over HTTP, persists results — pure client, never imports the solver
Solver solver (ml-tabu-uctp, from GHCR) Stateless, independently scalable, referenced by pinned tag — the code is consumed, never copied
Data postgres (+ volume) Users, orgs, catalogue, instances, jobs, timetables

Design decisions that matter: solving takes seconds-to-minutes, so the API enqueues and a worker consumes (no request blocking); the solver is stateless so it scales horizontally, independently of the app; images are multi-stage & non-root; the app talks to the solver only through a frozen HTTP contract, so either side can evolve independently.

docker compose up -d --build      # the whole platform, one command
curl localhost:8080/api/health    # {"status":"ok"}

The solver boundary & GHCR

The optimizer lives in a separate repository and is delivered as a container image on the GitHub Container Registry — Planify consumes it, it never vendors the code:

  • ghcr.io/idrissziadi/ml-tabu-uctp:0.1.0public, pulled anonymously by the worker.
  • Contract: POST /solve with a JSON instance → { feasible, hard, soft, timetable[] }.
  • Improve the solver → publish a new tag → bump SOLVER_TAG. Zero coupling.

Planify's own images are built and published to GHCR by CI (.github/workflows/release.yml): ghcr.io/idrissziadi/uctp-{backend,worker,frontend} — tagged latest and a short git SHA for deterministic rollbacks.


Security

Security is designed in, not bolted on — and every claim below is backed by tests and CI.

  • Multi-tenant isolation (defense-in-depth). Every domain query flows through one OrgScopedRepository that filters by org_id; cross-tenant access returns 404, proven by a dedicated isolation gate in the test suite.
  • RBAC, least privilege. super_admin / admin / teacher; teachers are strictly read-only (writes → 403), audited by test_permissions_audit.py.
  • AuthN. Argon2 password hashing; server-side sessions in Redis (opaque token in an httpOnly, SameSite, Secure cookie); logout purges the Redis session (real invalidation, not just a cookie clear).
  • Abuse resistance. Redis fixed-window rate limiting on POST /solve (per user) and POST /login (per IP) → 429.
  • Hardened surface. Security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy) applied at three layers (FastAPI middleware, Caddy, Next.js); opt-in strict CORS; server-enforced password policy; non-root containers; invitation-only onboarding (no public sign-up).
  • Supply chain. Trivy filesystem scan (vulns, secrets, misconfig) runs as a CI job; secrets never live in images or git (env-injected); the solver tag is pinned.
  • Deployment. TLS + HSTS at the edge (Coolify/Traefik, Let's Encrypt).

See it live: for i in $(seq 12); do curl -s -o /dev/null -w "%{http_code} " -X POST \ localhost:8080/api/auth/login -d '{"email":"x@y.dz","password":"nope"}'; done401 … 401 429 429.


Testing — built test-first (TDD)

Every feature was written red → green → refactor. The suites are fast, deterministic, and gate every merge:

Suite Tests Highlights
Backend (pytest) 62 tenant-isolation gate, RBAC audit, §0.3 assembler/validator, solve→job flow, Alembic migrations vs real Postgres
Worker (pytest) 11 solver client (200/422/413/network), retry/backoff policy, outcome→job mapping
Frontend (vitest + Testing Library) 50 typed API client, auth guard, forms/zod, CRUD, timetable grid & export helpers

123 tests, all green — locally and in CI. The full stack was also validated end-to-end against the real solver image: a seeded instance is created via the API, enqueued, solved by the containerized Tabu Search solver, and the persisted timetable is asserted (feasible, hard == 0).


CI/CD & Deployment

Continuous Integration (ci.yml) — four parallel jobs on every push and PR: backend, worker, frontend (build), and security (Trivy).

Continuous Delivery (release.yml) — builds and pushes the three app images to GHCR (matrix, layer-cached, sha+latest tags).

Deployment — target is VPS + Coolify (self-hosted PaaS): docker-compose.prod.yml pulls the prebuilt GHCR images; Coolify/Traefik handles TLS + HSTS, secrets, automated Postgres backups, healthchecks, and rolling deploys with one-tag rollback. Alembic migrations run inside the backend image at deploy time. Full runbook: DEPLOYMENT.md.

push main ─► CI (tests + Trivy) ─► release (build+push GHCR) ─► Coolify pulls & deploys ─► TLS/HSTS

The build journey — Phases A → G

A disciplined, phase-by-phase build: each phase = brainstorm → written TDD plan → execute → review → merge, only advancing when green.

Phase What Outcome
A Solver (separate ML research repo) ML-Enhanced Tabu Search, packaged as an HTTP service + GHCR image
B Design Multi-tenant architecture, RBAC, async jobs, tenant-isolation strategy locked
C Scaffold Monorepo (frontend/backend/worker/proxy), Docker Compose, green CI
D Backend (TDD) D-1 auth/RBAC/sessions/invitations · D-2 catalogue CRUD · D-3 instances/events, §0.3 assembler, /solve + worker, real-solver e2e
E Frontend E-1 app shell + auth · E-2 catalogue · E-3 instances/events + solve · E-4 timetable grid + PDF/CSV; super-admin & teacher role-aware UIs
F Hardening Rate-limiting, session purge, password policy, CORS, security headers, permissions audit, worker retry, Trivy — security review
G Deployment GHCR image pipeline, production compose, Coolify runbook, TLS/HSTS/backups/rollbacks

Quick start

git clone https://github.com/idrissziadi/uctp-app && cd uctp-app
cp .env.example .env                 # COOKIE_SECURE=false for local http
docker compose up -d --build         # frontend, backend, worker, solver, redis, postgres, proxy
docker compose run --rm -e DATABASE_URL="postgresql+psycopg://uctp:uctp@postgres:5432/uctp" \
  backend alembic -c alembic.ini upgrade head          # migrations

Open http://localhost:8080. Demo accounts (org ESI):

Role Email Password
Admin admin@esi.dz Admin2026!
Teacher (read-only) prof@esi.dz Prof2026!
Super-admin superadmin@uctp.dz SuperAdmin2026!

Run the tests:

cd backend  && pytest -q      # 62
cd worker   && pytest -q      # 11
cd frontend && npm test       # 50

Tech stack

Layer Technology
Frontend Next.js 15 (App Router), TypeScript, Tailwind, shadcn/ui, TanStack Query, react-hook-form + zod
Backend FastAPI, SQLAlchemy 2, Alembic, Pydantic, Argon2, redis-py
Worker Python, httpx, psycopg
Data / Queue PostgreSQL 16, Redis 7
Solver ml-tabu-uctp (ML-Enhanced Tabu Search), consumed via HTTP
Infra Docker (multi-stage, non-root), Docker Compose, Caddy, GHCR, GitHub Actions, Trivy, Coolify
Testing pytest, Vitest, Testing Library, fakeredis

Project structure

uctp-app/
├─ frontend/         Next.js app (components, hooks, lib, tests)
├─ backend/          FastAPI (routers, models, repository, migrations, tests)
├─ worker/           Redis-consuming solver client (tests)
├─ proxy/            Caddyfile (routing + security headers)
├─ docker-compose.yml        # local / all-in-one
├─ docker-compose.prod.yml    # GHCR images, Coolify
├─ .github/workflows/         # ci.yml, release.yml
├─ DEPLOYMENT.md              # production runbook
└─ assets/                    # architecture diagram + screenshots

License

MIT © idrissziadi

About

Web app for university course timetabling that consumes the ml-tabu-uctp solver service (Docker) — data entry + timetable viewer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages