- Python 86.3%
- HTML 13.4%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build / build (push) Successful in 22s
Traccar scopes the device list per user via tc_user_device, and openid.adminGroup does not bypass it: a fresh OIDC admin logs in and sees zero devices. Grouping makes admin visibility one share per user instead of one per device per user. - settings.traccar_device_group, default "All Vehicles", empty disables - TraccarClient.ensure_group resolves or creates the group, memoised per client and tolerant of a lost create race - create_device takes group_id; ensure_device resolves the group first - a group lookup failure logs and provisions the device ungrouped rather than failing the provision - transport seam on the client so the new tests can drive httpx.MockTransport |
||
| .forgejo/workflows | ||
| example_data | ||
| scripts | ||
| src/cafe_car | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| .mcp.json | ||
| .pre-commit-config.yaml | ||
| CLAUDE.md | ||
| CURRENT_PLAN.md | ||
| Dockerfile | ||
| LICENSE.txt | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
cafe-car
Core API for GTFS.Zone: serves GTFS-RT feeds and provides an admin UI for managing feeds, trackers and service alerts.
Part of a larger stack; see deploy-gtfs-rt for the full deployment.
How it fits together
GitHub / Google / GitLab
└─> Keycloak (OIDC provider, brokers all three onto one account)
└─> oauth2-proxy (ForwardAuth)
└─> Traefik
├─> Admin app (manage.rt.<domain>), auth-gated
└─> Public API (rt.<domain>), no auth
├─> PostgreSQL (feeds, trackers, alerts, users)
└─> Redis DB 1 (vehicle positions, trip updates)
Traccar Client app (phone) / GPS unit
└─> Traccar (/osmand)
└─> vehicle-poser (HTTP forward) → Redis DB 1 (vehicle:{tracker_id}:* keys)
hell-gate-bridge (Amtrak, Columbia County)
└─> POST /ingest/position, /ingest/trip-update → Redis DB 1
trip-updogger
└─> sweeps vehicle:* + scheduled stop_times → Redis DB 1 (trip_update:{trip_id} keys)
There is no MQTT broker and no OwnTracks path any more: positions arrive over HTTP, either through vehicle-poser (Traccar's forwarder) or directly on this service's /ingest API. Trip delays are written by trip-updogger and by upstream pollers.
Trackers, not drivers
A Tracker is one vehicle's credential. Its id is a secret pet-name (e.g. gently-tender-oyster) that doubles as the Traccar uniqueId, the Redis key namespace, and the tracker_id a producer posts under. There is no password. Creating a tracker in the admin auto-creates the matching Traccar device and renders a provisioning QR for the Traccar Client app.
The id is never emitted in a feed. Vehicles are labelled with the tracker's public nickname instead.
A TrackerRule binds a tracker to a trip_id on a day-of-week and time window, which is how vehicle-poser resolves an incoming position to a trip server-side.
Public API endpoints
| Endpoint | Description |
|---|---|
GET /{feed_name}/vehicle_positions.pb |
Live vehicle positions (GTFS-RT protobuf) |
GET /{feed_name}/trip_updates.pb |
Trip updates from Redis (GTFS-RT protobuf) |
GET /{feed_name}/service_alerts.pb |
Service alerts from Postgres (GTFS-RT protobuf) |
GET /{feed_name}/*.json |
The same three feeds as JSON, for browsers and debugging |
GET /feeds |
Public feed catalog: every feed, its four URLs, and whether each realtime endpoint currently has anything in it |
GET /health |
Liveness check (pings Redis + Postgres) |
All of the above are unauthenticated. Feeds are configured in the admin UI.
Ingest API
Service-to-service, guarded by a shared bearer token (INGEST_API_TOKEN), for producers that already know their own trip_id:
| Endpoint | Description |
|---|---|
POST /ingest/position |
One vehicle position, written as a vehicle:{tracker_id}:{slug} record with a 60s TTL |
POST /ingest/trip-update |
One trip's delay/stop-time predictions, 300s TTL |
POST /ingest/alerts |
Replace a feed's producer-published service alerts |
GET /feed_urls is internal-only: it refuses any request carrying X-Forwarded-For.
Local development
Requires a .env file:
DATABASE_URL=postgresql+asyncpg://postgres:mysecretpassword@localhost:5432/postgres
REDIS_URL=redis://localhost:6379/1
SESSION_SECRET_KEY=some-random-secret-key
Run Postgres and Redis externally (e.g. via the deployment stack), then:
uv sync
uv run fastapi dev src/cafe_car/main.py # public API → http://localhost:8000
uv run fastapi dev src/cafe_car/admin_main.py # admin app → http://localhost:8001
The admin app mounts at /, not at /admin. To simulate oauth2-proxy headers locally:
curl -H "X-Auth-Request-User: alice" -H "X-Auth-Request-Email: alice@example.com" \
http://localhost:8001/
X-Auth-Request-User is the OIDC subject and is the only thing identifying the caller; the header alone creates the User and Identity on first use. Paths that need a verified email (invite claiming, account linking) also want a token: see CLAUDE.md for the unsigned-JWT recipe under DEBUG=true.
Testing a feed
Helper scripts live under scripts/.
simulate_trip.py
Simulates real GTFS trips along their shapes, POSTing positions to /ingest/position. The simulation starts where the vehicle would actually be right now according to the schedule, with a random delay.
# List what is in the GTFS zip:
uv run scripts/simulate_trip.py --list-routes
uv run scripts/simulate_trip.py --list-trips
# Simulate trip WCCWB at 10x speed, publishing every 2s:
uv run scripts/simulate_trip.py --trip WCCWB
# Every trip on a route, or N random trips, or the whole feed:
uv run scripts/simulate_trip.py --route 1 --route 2
uv run scripts/simulate_trip.py --n-trips 10 --speed 20
uv run scripts/simulate_trip.py --all-trips --speed 50 --quiet
# Custom tracker, ingest endpoint, speed and interval:
uv run scripts/simulate_trip.py --tracker <tracker-id> \
--ingest-url http://localhost:8000 --token dev-ingest-token \
--trip ELLSWB --speed 30 --interval 1
The tracker must exist in the database (created via the admin UI), and the token must match INGEST_API_TOKEN, for the positions to appear in the feed.
provision_source.py
Creates the row chain a producer needs (a Feed owned by an existing User, plus a Tracker), creates the matching Traccar device, and prints the tracker id to paste into the producer's env. Idempotent.
Feed inspectors
fetch_vehicles.py, fetch_trip_updates.py and fetch_service_alerts.py each fetch one .pb endpoint and pretty-print it; consumer_tool.py does all three, and can follow a feed and map it.
uv run scripts/fetch_vehicles.py <feed_name> # full protobuf dump
uv run scripts/fetch_vehicles.py <feed_name> --summary # one line per vehicle
uv run scripts/fetch_vehicles.py <feed_name> --backend http://localhost:8000
Development commands
# Install git hooks (required once per clone)
uv run pre-commit install
uv run ruff check src/ # lint
uv run ruff check --fix src/ # lint + autofix
uv run pytest # run tests
# Apply migrations. Models and Alembic revisions live in railroad-club, which
# ships the migrator as a console script; this repo has no alembic.ini.
uv run railroad-club-migrate