Real-time public transport information for Stockholm — live departure boards, nearby stops, service alerts, and journey planning across all SL transport modes.
A full-stack application connecting Stockholm's public transit data (SL Trafiklab APIs) to a responsive dark-theme dashboard. Built with React + TypeScript on the frontend and Python FastAPI on the backend, containerized with Docker, and deployed via CI/CD.
graph TB
subgraph Client["Client Layer (Browser)"]
UI["React SPA<br/>Vite + TypeScript"]
STORE["localStorage<br/>Favorites · Recents · Cache"]
end
subgraph Server["Server Layer (FastAPI / Python)"]
API["REST API<br/>/api/* endpoints"]
WS["WebSocket Server<br/>/api/alerts/ws/{id}"]
POLLER["Background Alert Poller<br/>asyncio"]
SL_CLIENT["SL API Client<br/>httpx + Pydantic"]
GEO["Geospatial Engine<br/>Haversine distance"]
end
subgraph External["External Layer"]
SL_APIS["SL Trafiklab APIs<br/>Typeahead · Realtime · Deviations · Journey Planner"]
SUPABASE["Supabase PostgreSQL<br/>Cloud favorites (optional)"]
end
UI -- "HTTP REST" --> API
UI -- "WebSocket" --> WS
UI --> STORE
UI -. "optional" .-> SUPABASE
API --> SL_CLIENT
API --> GEO
WS --> POLLER
POLLER --> SL_CLIENT
SL_CLIENT --> SL_APIS
Data flow: The React SPA communicates with the FastAPI backend via REST for departures, stop search, nearby stops, and journey planning. Live service alerts are pushed from the backend to the browser through a persistent WebSocket connection, fed by a background asyncio poller that fetches only for stops with active subscribers. The backend translates between SL's raw API responses and the frontend's expected schema, handling both key-based and free API modes transparently.
graph TB
subgraph Development["Development"]
DEV["Developer<br/>git push"]
end
subgraph GitHub["GitHub"]
CI["CI/CD Pipeline<br/>GitHub Actions"]
BUILD["Build Frontend<br/>npm ci + build"]
TEST["Run Tests<br/>Python unittest"]
DOCKER["Build Docker<br/>Image"]
TRIVY["Trivy Security<br/>Scan"]
CI --> BUILD
CI --> TEST
CI --> DOCKER
DOCKER --> TRIVY
end
subgraph Production["Production"]
RENDER["Render Deploy"]
TRIVY -- "pass" --> RENDER
end
subgraph Browser["Browser"]
FE["React SPA<br/>Vite + TypeScript"]
end
subgraph Backend["FastAPI Backend (Python)"]
API["REST API<br/>/api/*"]
WS["WebSocket<br/>/api/alerts/ws/{id}"]
POLLER["Background Poller<br/>asyncio"]
API --> POLLER
end
subgraph Storage["Storage"]
LS["localStorage<br/>Favorites · Recents"]
SB["Supabase<br/>Cloud Favorites"]
end
subgraph ExternalSL["SL Trafiklab Open Data APIs"]
SL1["SL Typeahead<br/>Stop Search"]
SL2["SL Realtidsinformation 4<br/>Live Departures"]
SL3["SL Deviations<br/>Service Alerts"]
SL4["SL Journey Planner<br/>Trip Planning"]
end
DEV --> GitHub
RENDER --> Backend
RENDER -.->|"serves static files"| FE
FE -- "HTTP REST" --> API
FE -- "WebSocket" --> WS
FE --> LS
FE -.-> SB
API --> SL1
API --> SL2
API --> SL3
API --> SL4
POLLER --> SL3
POLLER --> WS
High-level flow: Developer pushes to GitHub → CI/CD builds, tests, scans → deploys to Render → running app serves the React SPA (static) and FastAPI backend → backend proxies requests to SL Trafiklab Open APIs → live alerts pushed via WebSocket to the browser.
| Category | Technology | Purpose |
|---|---|---|
| Frontend | React 18 + TypeScript | Component-based UI with type safety |
| Build | Vite 5 | Fast HMR dev server, optimized production builds |
| Backend | Python 3.11+ / FastAPI | Async API server with automatic OpenAPI docs |
| API Client | httpx | Async HTTP client for upstream SL API calls, connection pooling |
| Validation | Pydantic / Pydantic-Settings | Request/response validation + type-safe .env config |
| Rate Limiting | slowapi | Per-endpoint rate limiting (30 req/min) |
| Database | Supabase (PostgreSQL) | Optional cloud favorites persistence |
| Container | Docker (multi-stage) | Node build → Python runtime image |
| CI/CD | GitHub Actions | Build, test, scan (Trivy), deploy (Render) |
- Backend: Python FastAPI — Chosen for its native
asynciosupport, which is essential for non-blocking HTTP calls to upstream SL APIs and concurrent WebSocket connections. Automatic OpenAPI docs provide a built-in client for debugging. - Frontend: React + TypeScript — TypeScript catches schema mismatches between SL API responses and the UI at compile time. The component model maps naturally to the card-based departure board UI.
- WebSocket for alerts — Service disruptions need real-time push. FastAPI's WebSocket support enables the backend to push filtered alerts only to subscribers of specific stops, avoiding client-side polling overhead.
- No React Router — The app is a single-view dashboard. State-driven conditional rendering in
App.tsxkeeps the bundle minimal and avoids URL complexity for a tool that has no navigable pages. - No CSS framework — All styles are inline
React.CSSProperties. This eliminates a CSS dependency, keeps the dark-theme styling self-contained in one codebase, and avoids class-name collisions. - localStorage-first persistence — Favorites and recent stops are stored client-side by default. Supabase cloud sync is an optional opt-in, keeping the core experience dependency-free.
- Centralized exception handling —
SLApiErroris caught by global handlers with structuredlogger.exception()logging. Route handlers no longer duplicate try/except blocks. Unhandled exceptions return consistent 500 responses with request context. - Graceful degradation — Every feature has a fallback: WebSocket → REST polling with exponential backoff (3 attempts before giving up); Supabase → localStorage; SL API key → free/open endpoints.
- Background poller isolation — The alert poller runs as an independent asyncio task. If it crashes, it restarts without affecting REST endpoints. Subscribers per stop are tracked so the poller only fetches data for actively viewed stops. Exponential backoff (1s → 2s → 4s … 120s cap) prevents thundering-herd on recovery.
- Health monitoring — The backend exposes
/api/health; the frontend polls it every 30 seconds and displays a color-coded indicator, giving immediate visibility into connectivity issues.
- API key isolation — SL API keys are used only server-side via Pydantic Settings (
.env). The frontend never sees raw keys, preventing client-side exposure. Both key-based and free modes are supported server-side with a simple query parameter switch. - Rate limiting — All REST endpoints are rate-limited to 30 requests/minute per client IP via
slowapi, protecting upstream SL API quotas. - CORS hardening — Configurable
cors_originsin settings. Wildcard origins (*) automatically disableallow_credentialsto mitigate CWE-942. - No authentication — The app is intentionally stateless and authentication-free. Favorites are personal (localStorage) or opt-in (Supabase anon key). This eliminates credential management and attack surface.
- Connection pooling — A shared
httpx.AsyncClient(20 keepalive connections, 100 max) is created during app lifespan and injected viaDepends(), avoiding per-request client creation. - Selective polling — The alert manager polls only for stops with active WebSocket subscribers, minimizing upstream API calls and server resource usage.
- 30-second refresh cadence — Departure boards auto-refresh at 30s, balancing freshness against SL API rate limits. Manual refresh is always available for immediate updates.
- Minimal bundle — No router, no CSS framework, no state management library. The frontend ships only what it uses, keeping initial load time low.
Real-time departures grouped by transport mode (Bus, Metro, Train, Tram, Ship). Each card shows line number, destination, scheduled/expected time, and deviation status. Auto-refreshes every 30 seconds with manual refresh always available.
Async autocomplete search against SL's stop database. Returns stops with type metadata and site IDs. Recent stops (last 4) persist in localStorage for one-tap re-access.
Browser geolocation or manual text input to find stops ranked by Haversine distance. Each nearby stop shows a live departure preview with mode filtering. Selection loads the full board.
Point-to-point trip planner with stop autocomplete for both origin and destination. Returns up to 5 trip options sorted by departure time, with expandable leg details (mode, line, duration, transfers).
Service alerts pushed via WebSocket with severity color-coding:
- Critical (red) — Major disruptions
- Warning (amber) — Significant delays
- Info (blue) — Minor changes / planned work
Backend runs a background asyncio task polling only for stops with active WebSocket subscribers.
Real-time status pill in the header polls /api/health every 30 seconds. Green/amber/red indicators let users know if the backend is reachable.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Backend health check |
GET |
/api/realtime/search?query={text} |
Search stops/stations |
GET |
/api/realtime/liveboard/{site_id} |
Raw departure data |
GET |
/api/liveboard/format/{site_id} |
Formatted departure board |
GET |
/api/nearby/stops?lat={}&lon={} |
Nearby stops ranked by distance |
GET |
/api/nearby/boards?lat={}&lon={} |
Nearby stops with departure previews |
GET |
/api/nearby/train-boards?lat={}&lon={} |
Nearby train/metro stations with previews |
GET |
/api/situations/?site_id={} |
Service alerts (REST) |
GET |
/api/alerts/?site_id={} |
Alerts REST fallback |
WS |
/api/alerts/ws/{site_id} |
Live alert push |
POST |
/api/journey/plan |
Journey planning (JSON body) |
- Node.js 18+
- Python 3.11+
- SL API key (Trafiklab) — optional for free mode
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Configure (optional — app runs in free mode without a key)
cp .env.example .env
# Edit .env with your SL_REALTIME_API_KEY
# API docs at http://localhost:8000/docs
uvicorn main:app --reload --port 8000npm install
npm run dev # Starts on localhost with API proxy to backend
### Docker
```bash
docker build -t stockholm-travel-planner .
docker run -p 8000:8000 stockholm-travel-plannergraph LR
Push[Push to main] --> CI[GitHub Actions]
CI --> Build[Build Frontend<br/>npm ci + npm run build]
CI --> Test[Run Tests<br/>Python unittest]
CI --> Docker[Build Docker Image]
Docker --> Scan[Trivy Vulnerability Scan]
Scan --> Deploy[Render Deploy Hook]
Deploy --> Live[Production]
The pipeline in .github/workflows/ci.yml:
- Installs Node dependencies and builds the frontend
- Runs Python test suite (7 test files)
- Builds the multi-stage Docker image
- Scans with Trivy for vulnerabilities
- Triggers Render deploy hook (only on
main, only if scan passes)
├── backend/
│ ├── main.py # FastAPI app (lifespan, middleware, exception handlers)
│ ├── routers/
│ │ ├── realtime.py # Stop search + raw departures
│ │ ├── liveboard.py # Formatted departure boards
│ │ ├── nearby.py # Geospatial nearby queries
│ │ ├── situations.py # Service alerts REST
│ │ ├── alerts.py # Alerts REST + WebSocket endpoint
│ │ └── journey.py # Journey planning
│ ├── services/
│ │ ├── config.py # Pydantic Settings (`.env` loading, all config)
│ │ ├── dependencies.py # FastAPI DI (get_http_client, limiter)
│ │ ├── schemas.py # Pydantic response models for every endpoint
│ │ ├── exceptions.py # SLApiError definition
│ │ ├── sl_api.py # Core SL HTTP client, Haversine distance, normalizers
│ │ ├── sl_config.py # Configurable API URLs (wraps config.py)
│ │ ├── alerts_service.py # Alert normalization
│ │ ├── alerts_manager.py # WebSocket manager + background poller (exponential backoff)
│ │ └── journey_service.py # Trip normalization
│ └── tests/
│ ├── test_*.py # 7 test files (unittest, 68 tests)
│ └── scripts/ # Standalone smoke-test scripts
├── src/
│ ├── App.tsx # Application shell, state, layout
│ ├── main.tsx # React entry point
│ ├── components/
│ │ ├── SearchBar.tsx # Stop autocomplete
│ │ ├── stopBoard.tsx # Live departure board + mode filters
│ │ ├── LiveBoardCard.tsx # Individual departure card
│ │ ├── NearbyStops.tsx # Nearby stops panel
│ │ ├── FavoritesList.tsx # Saved stops
│ │ ├── DisruptionBanner.tsx# Live alerts
│ │ └── JourneyPlanner.tsx # Trip planner
│ ├── hooks/
│ │ ├── useLocalFavorites.ts# localStorage favorites
│ │ ├── useAlerts.ts # WebSocket + REST fallback
│ │ └── useMediaQuery.ts # Responsive breakpoints
│ └── types/index.ts # TypeScript interfaces
├── Dockerfile # Multi-stage build
├── .github/workflows/ci.yml # CI/CD pipeline
└── supabase/migrations/ # Database schema
MIT