Waitport monitors real-time security queue wait times and provides ML-based predictions for 11 European airports. Queue data is collected every 5 minutes, stored in PostgreSQL (and mirrored to Supabase), and used to train per-airport XGBoost models that predict future queue lengths.
| Code | City | Country |
|---|---|---|
| CPH | Copenhagen | Denmark |
| FRA | Frankfurt | Germany |
| ARN | Stockholm Arlanda | Sweden |
| OSL | Oslo | Norway |
| DUS | Dusseldorf | Germany |
| AMS | Amsterdam Schiphol | Netherlands |
| DUB | Dublin | Ireland |
| LHR | London Heathrow | United Kingdom |
| MUC | Munich | Germany |
| EDI | Edinburgh | United Kingdom |
| IST | Istanbul | Turkey |
flowchart TB
subgraph External
User["User Browser"]
AirportAPIs["Airport APIs\n(11 airports)"]
end
subgraph Docker Compose
subgraph Ingress
Nginx["Nginx\nReverse Proxy\n:80"]
end
subgraph Frontend
NextJS["Next.js 16\nReact 19 / Tailwind / i18n\n:3000"]
Legacy["Legacy React\n(backup)\n:3000"]
end
subgraph API Gateway
APISIX["Apache APISIX\n:9080"]
APISIXDash["APISIX Dashboard\n:9000"]
etcd["etcd\n:2379"]
end
subgraph Data Layer
Postgres["PostgreSQL 17\n:5432"]
PostgREST["PostgREST\n:3000"]
Swagger["Swagger UI"]
end
subgraph Machine Learning
MLAPI["Flask ML API\nXGBoost\n:5000"]
AutoGluon["AutoGluon\nChronos-2"]
end
subgraph Data Ingestion
Ofelia["Ofelia\nScheduler"]
Fetchtime["Fetchtime\nPython 3.12"]
end
end
subgraph Cloud
Supabase["Supabase"]
end
User -->|"http://localhost"| Nginx
Nginx -->|"/"| NextJS
NextJS -.->|"failover"| Legacy
Nginx -->|"/api/"| APISIX
APISIX --> PostgREST
APISIX --> MLAPI
APISIXDash --> APISIX
etcd <-->|"config"| APISIX
PostgREST --> Postgres
Swagger --> PostgREST
Ofelia -->|"@every 5m\nper airport"| Fetchtime
AirportAPIs -->|"queue data"| Fetchtime
Fetchtime -->|"write"| Postgres
Fetchtime -->|"write"| Supabase
MLAPI -->|"read"| Supabase
| Layer | Technologies |
|---|---|
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Recharts 3, Headless UI, i18n (en/da/de) |
| Backend | Python 3.12, Flask, PostgREST |
| ML | XGBoost 3.0, scikit-learn 1.7, AutoGluon, Chronos-2, holidays |
| Database | PostgreSQL 17, Supabase (cloud mirror) |
| API Gateway | Apache APISIX, etcd |
| Infrastructure | Docker Compose (13 services), Nginx, Ofelia scheduler, GitHub Actions CI/CD, GHCR |
- Docker and Docker Compose
- A Google Firebase service account key (
keyfile.json) for the data fetching service - Supabase project URL and service role key (for ML API training data)
The .env file in the repository root contains all required configuration. Key variables:
| Variable | Description |
|---|---|
POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_USER |
PostgreSQL credentials |
GCP_KEY_PATH |
Path to your Firebase keyfile.json |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY |
Supabase connection details |
CPHAPI_HOST |
Internal PostgREST endpoint |
*_HEALTHCHECK |
Healthcheck ping URLs per airport (CPH, FRA, ARN, etc.) |
docker compose up -dOnce running, access the frontend at http://localhost (port 80 via Nginx).
| Service | Image | Purpose | Exposed Port |
|---|---|---|---|
cph_postgres_db |
postgres:17 |
Primary database | - |
cph_frontend_nextjs |
ghcr.io/.../cph-security_nextjs:main |
Next.js frontend (primary) | - |
cph_frontend_nextgen |
ghcr.io/.../cph-security_frontend-nextgen:main |
Legacy React frontend (backup) | - |
reverse-proxy |
nginx:alpine |
Reverse proxy and load balancer | 80 |
cph_postgrest |
postgrest/postgrest |
Auto-generated REST API from PostgreSQL | - |
swagger-ui |
swaggerapi/swagger-ui |
Interactive API documentation | - |
universal_fetchtime |
ghcr.io/.../cph-security_fetchtime:main |
Scheduled data collection from airport APIs | - |
ofelia_scheduler |
mcuadros/ofelia |
Cron-like scheduler for fetchtime jobs | - |
apisix |
apache/apisix |
API gateway | 9080 |
apisix_dashboard |
apache/apisix-dashboard |
APISIX management UI | 9000 |
etcd |
bitnamilegacy/etcd |
APISIX configuration store | 2379 |
ml_api |
ghcr.io/.../cph-security_ml_api:main |
XGBoost prediction API | 5000 |
autogluon |
ghcr.io/.../cph-security_autogluon:latest |
Chronos-2 time-series forecasting | - |
Ofelia orchestrates the data collection jobs, running a fetch command for each airport every 5 minutes:
labels:
ofelia.enabled: "true"
ofelia.job-exec.cph.schedule: "@every 5m"
ofelia.job-exec.cph.command: "cph-fetch"
ofelia.job-exec.fra.schedule: "@every 5m"
ofelia.job-exec.fra.command: "fra-fetch"
# ... repeated for all 11 airportsThe API is powered by PostgREST, which generates a RESTful API directly from the PostgreSQL schema. Requests are routed through Nginx and APISIX.
Get all waiting times:
GET /api/v1/waitingtime
Get times with queue less than 4 minutes:
GET /api/v1/waitingtime?queue=lt.4
Get only queue and timestamp columns:
GET /api/v1/waitingtime?select=queue,timestamp
Get the latest waiting time:
GET /api/v1/waitingtime?select=queue&order=id.desc&limit=1
The ML API provides queue length predictions:
GET /predict?airport=CPH×tamp=2026-03-14T08:00
Parameters:
airport- Airport code (one of: AMS, ARN, CPH, DUB, DUS, FRA, IST, LHR, EDI, MUC)timestamp- Future datetime in UTC (format:YYYY-MM-DDTHH:MM)
Response:
{"predicted_queue_length_minutes": 12.5}PostgREST uses the OpenAPI standard to generate API documentation, consumed by Swagger UI. The Swagger UI container provides an interactive interface to explore and test the API endpoints.
The system uses two ML approaches:
XGBoost (primary) - Per-airport gradient-boosted models trained on historical queue data. Features include:
- Time-of-day, day-of-week, month
- Local public holidays (per country)
- Rolling averages and lag features
- Models retrain daily at midnight via Flask-Crontab
AutoGluon / Chronos-2 - Time-series forecasting using Amazon's Chronos-2 foundation model for complementary predictions.
Both services read training data from Supabase and serve predictions via REST endpoints.
The frontend supports three languages:
- English (
en) - Danish (
da) - German (
de)
Locale files are located in waitport/src/locales/ and the app automatically detects the user's preferred language from browser settings and URL path segments.
Five GitHub Actions workflows automate builds and security scanning:
| Workflow | Trigger | Action |
|---|---|---|
frontend_nextjs.yml |
Changes to waitport/ |
Build and push Next.js image to GHCR |
fetchtime.yml |
Changes to fetchtime/ |
Run tests, build and push image to GHCR |
ml_api.yml |
Changes to ml_api/ |
Build and push ML API image to GHCR |
autogluon.yml |
Changes to autogluon/ |
Build and push AutoGluon image to GHCR |
codeql.yml |
Scheduled / on push | Code security analysis |
All production images are hosted on GitHub Container Registry (ghcr.io/simonottosen/cph-security_*).
cph-security/
├── waitport/ # Next.js frontend (React 19, Tailwind, i18n)
├── fetchtime/ # Data collection service (Python 3.12)
├── ml_api/ # XGBoost prediction API (Flask)
├── autogluon/ # Chronos-2 forecasting service
├── nextflight/ # Flight data service
├── database/ # PostgreSQL schema (create_tables.sql)
├── config/
│ ├── reverse_proxy/ # Nginx configuration
│ ├── apisix_conf/ # APISIX gateway configuration
│ └── apisix_dashboard_conf/
├── .github/workflows/ # CI/CD pipelines
└── docker-compose.yml # Service orchestration (13 services)
This project is licensed under the MIT License - see the LICENSE file for details.