A lightweight service to aggregate, store, and serve public transport stops from various countries and sources. This project provides a unified API to access stop data and includes a simple web viewer.
- Data Aggregation: Fetches and merges public transport stops from multiple international sources (GTFS, APIs, etc.).
- Unified API: Provides a RESTful API to query stops by bounding box or paginated lists.
- Web Viewer: Includes a built-in HTML/JS frontend to visualize stops in a table format.
- Dockerized: Fully containerized with Docker and Docker Compose for easy deployment.
Note: This repository has been converted to use SQLite by default (for simpler deployment). The compose setup now runs the API alone and persists a SQLite file in a named volume. If you prefer PostgreSQL, set DATABASE_URL to a Postgres DSN and run a Postgres service separately.
The service currently aggregates data from the following sources:
- United Kingdom (BusTimes.org)
- Finland (Digitransit - National, HSL, Varely, Waltti)
- France (Transport.data.gouv.fr)
- Germany (GTFS.de)
- Italy (BusMaps)
- Slovakia (BusMaps)
- Poland (BusMaps)
- Greece (BusMaps)
- Switzerland (OEV-Info)
- Netherlands (OVapi)
- Luxembourg (Open Data Luxembourg)
- Jersey (BusTimes.org)
- Sweden (Resrobot.se)
- Backend: Python 3.9+, FastAPI
- Database: SQLite (default) — optional PostgreSQL via
DATABASE_URL - ORM/Data Access: SQLAlchemy,
aiosqlitefor async utilities - HTTP Client: httpx
- Containerization: Docker, Docker Compose
Some data sources require API keys. Create a file named apikey.env in the backend/sources/ directory to store these keys.
File: backend/sources/.env
SWEDEN_KEY=your_sweden_api_key_here-
Clone the repository:
git clone https://github.com/Kai-codin/stops-service.git cd stops-service -
Build & start the API (SQLite-backed):
docker compose up --build api
The Docker Compose config now runs only the
apiservice by default and persists the SQLite database at/var/lib/stops/stops.dbinside the container (stored in the named volumestops_data). -
Full compose (if you have custom services):
docker compose up --build
-
Access the application:
- API Root: http://localhost:8991
- Stops Viewer: http://localhost:8991/stops
- Data Stats: http://localhost:8991/data
Notes:
- To use an external PostgreSQL instance instead, set the
DATABASE_URLenvironment variable to your Postgres DSN (e.g.postgresql://user:pass@host:5432/db) incompose.yamlor your environment; the app will detect non-sqlite DSNs and use SQLAlchemy's engine accordingly. - If you want to persist the SQLite DB to a host folder instead of a named volume, modify
compose.yamlto use a host bind mount (I can update that for you if desired).
GET /api/stops?xmin={min_lon}&xmax={max_lon}&ymin={min_lat}&ymax={max_lat}&limit={limit}Parameters:
xmin,xmax: Longitude bounds.ymin,ymax: Latitude bounds.limit: (Optional) Max number of stops to return (default: 10000).
GET /api/allstops?limit={limit}&offset={offset}Parameters:
limit: (Optional) Number of stops per page (default: 5000).offset: (Optional) Pagination offset (default: 0).
The project includes a utility script to fetch and merge data from sources. This is typically run within the backend container or as a separate task.
To run the merge/import script manually (inside the backend container or with a proper Python environment):
# inside the backend container or from the backend folder
python -m utils.merge
# or run just a single source
python -m utils.merge luxembourgstops-service/
├── compose.yaml # Docker Compose configuration
├── readme.md # Project documentation
└── backend/ # Backend application code
├── Dockerfile # Backend Docker image definition
├── main.py # FastAPI entry point
├── requirements.txt # Python dependencies
├── sources/ # Source-specific fetcher modules
├── templates/ # HTML templates (Frontend)
└── utils/ # Utility scripts (merge, dump)