Lightweight Node.js proxy for transport.rest and RSS feeds with CORS, caching, timeouts, and in-flight request deduplication.
- Node.js 18+
- npm
screen(optional, for the start helper)
npm installnpm startThe server listens on port 56669 by default (set in package.json).
./start-screen.sh
screen -r startpage-proxysystemctl --user daemon-reload
systemctl --user enable --now startpage-proxy.serviceEnable lingering to start at boot without login:
sudo loginctl enable-linger srvmgrBase prefix: /api
GET /api/locations?query=Berlin&results=8&stops=true&addresses=false&poi=false
GET /api/stops/:id/departures
GET /api/stations/:id/departures
GET /api/departures?query=Kroepcke&limit=6
Direct GTFS-RT stop IDs can skip the transport.rest fallback:
GET /api/departures?stopId=gtfs:000009028694&limit=6
The endpoint returns a stable JSON shape:
{
"ok": true,
"source": "gtfs-rt",
"fallback": false,
"stop": { "id": "gtfs:000009028694", "name": "000009028694" },
"updatedAt": "2026-05-13T15:00:00.000Z",
"departures": [],
"diagnostics": { "provider": "vbn-gtfs-rt", "cache": "fresh" }
}If no live data is available, the endpoint returns a structured ok:false
response. If a recent good response exists, it returns that response with
source: "cache" and stale: true.
GET /api/transport/locations?query=Kroepcke&limit=8
Returns deduplicated location candidates with stable IDs and provider IDs.
GET /api/rss?url=<feed-url>
RSS JSON mode:
GET /api/rss?url=<feed-url>&format=json
All responses include:
Access-Control-Allow-Origin: *Access-Control-Allow-Headers: *
General proxy requests time out after 10 seconds. The smart location endpoint uses a shorter fallback timeout so a slow transport.rest response does not block otherwise local GTFS matching.
LOCATION_FALLBACK_TIMEOUT_MS=1200 npm startThe location fallback caches successful responses for 30 seconds, empty results
for 10 seconds, and can serve a successful response as stale for another five
minutes when the upstream fails. These durations can be changed with
LOCATION_FALLBACK_CACHE_MS, LOCATION_FALLBACK_NEGATIVE_CACHE_MS, and
LOCATION_FALLBACK_STALE_MS.
/api/locations: 30s/api/stops/:id/departures: 8s/api/stations/:id/departures: 8s/api/departures: 10s- last good
/api/departuresresponse: 15m /api/rss: 5m
The smart departures endpoint uses the gtfs.de static and realtime feeds as a matching pair. VBN GTFS-Realtime and transport.rest remain fallback providers.
Default gtfs.de realtime feed:
https://realtime.gtfs.de/realtime-free.pb
Default VBN feed:
http://gtfsr.vbn.de/gtfsr_connect.json
Override the feed URL:
GTFS_DE_REALTIME_URL=https://realtime.gtfs.de/realtime-free.pb npm start
VBN_GTFS_RT_JSON_URL=http://gtfsr.vbn.de/gtfsr_connect.json npm startFor query-to-stop matching, the proxy downloads and caches the static GTFS feed,
extracts stops.txt, and builds an in-memory stop index.
Default static GTFS feed:
https://download.gtfs.de/germany/free/latest.zip
Override the static feed or cache file:
GTFS_STATIC_URL=https://download.gtfs.de/germany/free/latest.zip npm start
GTFS_STATIC_CACHE_FILE=.cache/gtfs-static.zip npm start
GTFS_HEADSIGN_CACHE_FILE=.cache/gtfs-headsigns.json npm start
GTFS_STATIC_REFRESH_MS=86400000 npm startThe static ZIP is refreshed every 24 hours by default and replaced atomically. If a refresh fails, the last local ZIP remains available as stale fallback. Headsigns are cached persistently with the matching static-feed version. The API only waits briefly for missing headsigns so live responses stay fast while the background cache is being built.
Run checks:
npm test
npm run checkConfigure query aliases for known GTFS stop IDs:
TRANSPORT_STOP_ALIASES='{"kroepcke":["000009028694"]}' npm startUse TLS termination in Nginx and proxy to localhost:56669.
Example:
location / {
proxy_pass http://localhost:56669/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}