Curtain is a Flask service for URL shortening, redirect tracking, analytics, and incident-response demos. The current runtime architecture uses PostgreSQL for durable data, Redis for counters and cache, sharded Redis instances for real-time click collection, Nginx for load balancing, and Prometheus/Grafana for monitoring.
flowchart TD
U[User / Client]
N[Nginx<br/>Load Balancer]
subgraph APPS[Application Tier]
A1[app<br/>Flask + Gunicorn]
A2[app2<br/>Flask + Gunicorn]
end
APPS_OUT[app / app2<br/>shared app behavior]
PG[(PostgreSQL)]
RC[(redis<br/>URL Counter)]
CACHE[(redis_cache<br/>URL + Analytics Cache)]
S0[(redis_shard0<br/>Clicks + Streams)]
S1[(redis_shard1<br/>Clicks + Streams)]
SC[stream_consumer]
P[Prometheus]
G[Grafana]
NT[notifier]
DR[discord_relay]
D[Discord Webhook]
U -->|HTTP| N
N --> A1
N --> A2
A1 -.-> APPS_OUT
A2 -.-> APPS_OUT
APPS_OUT -->|DB reads/writes| PG
APPS_OUT -->|short-code counter| RC
APPS_OUT -->|cache lookup / populate| CACHE
CACHE -.->|cache miss -> DB| PG
APPS_OUT -->|redirect click writes| S0
APPS_OUT -->|redirect click writes| S1
S0 -->|Redis Streams| SC
S1 -->|Redis Streams| SC
SC -->|batched redirect events| PG
APPS_OUT -->|/metrics scrape targets| P
P -->|queries + dashboards| G
NT -->|poll /api/v1/alerts| P
NT -->|alert batch| DR
DR --> D
If you want a step-by-step explanation of how data moves through the system, read docs/ARCHITECTURE_EXPLAINATION.md.
- git
- Docker and Docker Compose
- Optional for local non-Docker runs:
uv, Python 3.13, PostgreSQL, and Redis
git clone https://github.com/S-Sigdel/Curtain.gituv sync
docker compose up --build -d
curl http://localhost:5000/healthdocker compose exec app uv run python scripts/reset_db.py
docker compose exec app uv run python scripts/seed_csv.pyuv sync --dev
uv run python run.py- Reliability: evidence/RELIABILITY_EVIDENCE.md
- Scalability: evidence/SCALABILITY_EVIDENCE.md
- Incident response: evidence/INCIDENT_RESPONSE_EVIDENCE.md
Run the test suite locally:
uv sync --dev
uv run pytest --cov=app --cov-report=term-missingRun it inside Docker:
docker compose exec app uv sync --dev
docker compose exec app uv run pytest -qFor test coverage report:
docker compose exec app uv run pytest --cov=app --cov-report=term-missingThese are the main browser-accessible visual entrypoints in the stack:
- Main app UI:
http://localhost:5000/ - Grafana dashboard:
http://localhost:3000 - Prometheus UI:
http://localhost:9090 - Discord relay health page:
http://localhost:8080/health
- Two Flask app containers:
appandapp2 - Each app container runs Gunicorn with 2 workers
- Nginx fronts both app containers and exposes the service on
http://localhost:5000
- PostgreSQL stores
users,urls, andevents redisstores the monotonic URL counter used for short-code generationredis_cachestores cached JSON responsesredis_shard0andredis_shard1store sharded redirect counters, hourly buckets, HyperLogLog unique-visitor sketches, and Redis Streams entries
stream_consumerdrains Redis Streams and writes redirect events to PostgreSQLprometheusscrapes/metricsfrom both app containersgrafanavisualizes app and incident-response metricsnotifierpolls Prometheus alerts and forwards new firing alertsdiscord_relaytranslates internal alert payloads into Discord webhook posts
POST /urlsvalidates the request body.- The app tries to allocate the next short code from Redis key
url:counter. - If the counter Redis is unavailable, the app falls back to PostgreSQL max-id state.
- The new URL is persisted in PostgreSQL.
- A
createdevent is written to PostgreSQL. - Related cache keys are invalidated.
GET /r/<short_code>or the alias redirect routes resolve the URL, using cached redirect metadata when available.- The app records the click into the owning Redis shard:
- total counter
- hourly bucket
- HyperLogLog unique visitors
- Redis Stream append
- The app writes an immediate
redirectevent to PostgreSQL. - The client receives a
302redirect to the original URL.
GET /urlsGET /urls/<id>GET /urls/<id>/analytics
These routes use redis_cache as a read-through cache and return X-Cache: HIT, MISS, or BYPASS depending on the path.
GET /healthGET /metricsGET /POST /shorten-ui
POST /users/bulkGET /usersGET /users/<id>POST /usersPUT /users/<id>DELETE /users/<id>
POST /urlsGET /urlsGET /urls/<id>PUT /urls/<id>DELETE /urls/<id>GET /r/<short_code>GET /urls/short/<short_code>GET /urls/<short_code>/redirect
GET /eventsGET /events/<id>POST /eventsGET /urls/<id>/analytics
Common settings are listed in .env.example.
Important runtime variables:
DATABASE_URLREDIS_URLCACHE_REDIS_URLREDIS_SHARDSENABLE_INCIDENT_DEBUG_ROUTESDISCORD_WEBHOOK_URLGRAFANA_ADMIN_USERGRAFANA_ADMIN_PASSWORD
Curtain/
├── app/
│ ├── __init__.py
│ ├── cache.py
│ ├── database.py
│ ├── observability.py
│ ├── redis_client.py
│ ├── shard_ring.py
│ ├── stream_consumer.py
│ ├── models/
│ ├── routes/
│ ├── services/
│ └── templates/
├── docs/
├── evidence/
├── loadtests/
├── monitoring/
├── nginx/
├── scripts/
├── tests/
├── docker-compose.yml
├── gunicorn.conf.py
├── pyproject.toml
└── run.py
- API examples: docs/API_EXAMPLES.md
- Error handling: docs/ERROR_HANDELING.md
- Failure modes: docs/FAILURE_MODES.md
- Incident response: docs/INCIDENT_RESPONSE.md
- Root-cause diagnosis: docs/DIAGNOST_ERRORS.md
- Load testing: docs/LOAD_TESTING.md
- Observability: docs/OBSERVABILITY.md
- Runbook: docs/RUNBOOK.md
- Redis details: docs/REDIS_INFO.md