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.
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 |
|---|---|---|
![]() |
![]() |
![]() |
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 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.0— public, pulled anonymously by the worker.- Contract:
POST /solvewith 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 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
OrgScopedRepositorythat filters byorg_id; cross-tenant access returns404, 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 bytest_permissions_audit.py. - AuthN. Argon2 password hashing; server-side sessions in Redis (opaque token in an
httpOnly,SameSite,Securecookie); logout purges the Redis session (real invalidation, not just a cookie clear). - Abuse resistance. Redis fixed-window rate limiting on
POST /solve(per user) andPOST /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"}'; done → 401 … 401 429 429.
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).
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
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 |
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 # migrationsOpen http://localhost:8080. Demo accounts (org ESI):
| Role | 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| 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 |
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
MIT © idrissziadi



