Lynx is a real-time public transport map for Lviv, Ukraine. It combines GTFS static data with GTFS-Realtime vehicle positions to show vehicles, stops, arrival estimates, route progress, and delay information.
- Live vehicle updates over WebSocket
- Bus, tram, and trolleybus markers on an interactive map
- Stop search and viewport-based stop loading
- Live ETA with schedule fallback
- Vehicle trip progress and delay estimates
- PostgreSQL, PostGIS, TimescaleDB, and Redis storage
- Mock vehicle mode when no GTFS-Realtime feed is configured
- Docker Compose development environment
The mobile view shows live vehicles on the map together with route progress, upcoming stops, arrival times, and delay estimates.
| Area | Technology |
|---|---|
| Frontend | React, Vite, Leaflet, React Leaflet |
| API | Node.js, Fastify |
| Live updates | Redis Pub/Sub, WebSocket |
| Database | PostgreSQL, PostGIS, TimescaleDB |
| Data | GTFS Static, GTFS-Realtime |
Requirements: Docker Desktop and Docker Compose.
cp .env.example .env
docker compose up --buildOpen:
- Frontend: http://localhost:5173
- API health check: http://localhost:3000/health
The included gtfs/ directory is imported automatically when the database
starts for the first time. If GTFS_RT_URL is empty, the API uses mock vehicle
positions.
To reset the database and import GTFS data again:
docker compose down -v
docker compose up --buildRequirements: Node.js 22+, PostgreSQL with PostGIS and TimescaleDB, and Redis.
cp .env.example .env
npm install
npm --prefix frontend install
npm run gtfs:import -- ./gtfsRun the backend and frontend in separate terminals:
npm run dev
npm run frontend:dev| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
REDIS_URL |
Redis connection string |
GTFS_RT_URL |
GTFS-Realtime vehicle positions URL; empty enables mock mode |
GTFS_RT_API_KEY |
Optional bearer token for the realtime feed |
POLL_INTERVAL_MS |
Realtime polling interval |
AGENCY_TIMEZONE |
GTFS agency timezone, defaults to Europe/Kyiv |
NODE_ENV |
development or production |
JWT_SECRET |
Reserved for authentication; replace before production use |
Never commit .env. Only .env.example belongs in the repository.
npm start # start the API
npm run dev # start the API in watch mode
npm run frontend:dev # start the Vite frontend
npm run frontend:build # build the frontend
npm run gtfs:import -- ./gtfs
npm run gtfs:download
npm test # syntax checks and frontend production buildGET /health
GET /vehicles
GET /vehicles/:vehicleId/track
GET /vehicles/:vehicleId/trip-progress
GET /routes
GET /routes/:routeId/stops
GET /routes/:routeId/vehicles
GET /stops
GET /stops/search?q=...
GET /stops/nearby?lat=...&lon=...
GET /stops/:stopId/eta
GET /stops/:stopId/schedule
GET /analytics/delays
GET /analytics/heatmap
WS /ws
frontend/ React and Leaflet client
src/
db/ PostgreSQL and Redis clients
routes/ REST and WebSocket routes
services/ ETA, GTFS import, and realtime polling
workers/ GTFS downloader
scripts/init.sql Database schema
gtfs/ Static GTFS files
docs/screenshots/ README images
- Replace development database credentials and
JWT_SECRET. - Set a trusted CORS origin instead of allowing every origin.
- Put the API and frontend behind HTTPS and a reverse proxy.
- Confirm that your GTFS data provider permits redistribution.
- Add API and integration tests.
- Add monitoring, backups, and rate limits appropriate for your traffic.
MIT