NJ Transit & PATH Realtime CLI
Objective
A local CLI that answers three questions from the terminal, without opening a trip planner:
- When is the next bus/train at a saved stop?
- Is it actually delayed, or is that a route-wide advisory being painted onto my trip?
- Has anything structurally changed on a line I depend on (stop deleted, deviation removed, schedule revised)?
Scope: NJ Transit bus (primary) and PATH. Rail is out of scope for v1.
Context
Triggered by a 2026-07-25 failure that took ~30 minutes to diagnose from a bus stop.
Google Maps stopped offering the 123 from 1410 Palisade Ave to Port Authority, and labelled the line “delayed” with no departures. Two independent causes, neither visible in the trip planner:
- A June 20, 2026 service change removed the Troy Towers and The Lenox deviations from route 123. NJT published this as a service advisory listing replacement stops with MyBus IDs. Downstream consumers appear to have lagged on the deleted stops, so routings anchored to them returned nothing.
- A route-wide advisory — up to 15-minute delays into/out of PABT from Route 495 traffic — was surfaced as per-trip status. It applies to every PABT-bound route and says nothing about any specific trip.
The failure mode is generic: consumer trip planners conflate route-level advisories with trip-level realtime, and they degrade silently when static data changes underneath them. Reading the feeds directly would have surfaced the June 20 alert weeks before it mattered, and would have distinguished “no vehicle is tracking” from “this route no longer stops here.”
Approach
Core design constraint: the two agencies are not symmetric
| Capability | NJ Transit | PATH |
|---|---|---|
| GTFS static | Yes — developer portal, token | Yes — PANYNJ |
| Arrival predictions at a stop | Yes | Yes |
| TripUpdates | Yes | Degraded (dummy trips) |
| VehiclePositions | Yes | No |
| Service alerts | Yes | No usable feed |
PATH has no first-party realtime worth consuming. The best available is a third-party bridge that fetches from the RidePATH path-data API (or the PANYNJ JSON API behind the schedules page) and republishes as GTFS-RT every 5 seconds, stop IDs matching the official static feed. But the Port Authority does not distribute the full realtime dataset — only stops and arrival times — so the bridge emits dummy trips with random IDs and a single stop-time update each. Enough for an arrivals board; useless for following a train through the system.
Consequence: model the common interface on arrivals at a stop, the lowest common denominator both agencies support. Vehicle positions and alerts become optional capabilities that only the NJT adapter declares. Do not design a trip-centric core — PATH cannot fill it.
Shape
- Python,
uv-managed, typed throughout.gtfs-realtime-bindingsfor protobuf,pydanticfor the domain model,httpx,typerfor the CLI. Agencyprotocol with explicit capability flags;NJTransitandPathadapters behind it. CLI degrades per-capability rather than erroring.- GTFS static cached locally in SQLite (stop name → stop_id lookup, route/direction resolution). NJT bus GTFS updates on their quarterly schedule cadence, so refresh is a scheduled job, not per-invocation.
- NJT token from
bitwarden-cli— see Store API secrets in password manger. No hardcoded keys, no.envin the repo. - Alert diffing is the feature that would have caught this incident: persist the alerts feed, diff on refresh, and surface new/changed alerts for watched routes. This is arguably more valuable than the arrivals board.
Sources to verify
| Source | Endpoint | Auth | Notes |
|---|---|---|---|
| NJT developer portal | developer.njtransit.com | Token | Replaced njtransit.com/developer-tools on 2024-08-01. GTFS, GTFS-RT, JSON REST |
| NJT MyBusNow (Clever Devices BusTime) | mybusnow.njtransit.com/bustime/map/getStopPredictions.jsp?route=&stop= | None | Undocumented, XML. Also getBusesForRoute.jsp, getRoutePoints.jsp. Can change without notice |
| PATH GTFS-RT bridge | path.transitdata.nyc/gtfsrt | None | Third-party, 5s refresh, degraded (see above) |
| PATH upstream | RidePATH path-data API / PANYNJ JSON API | None | What the bridge wraps. Self-hosting the bridge is an option |
| NJT advisories RSS | njtransit.com/rss/BusAdvisories_feed.xml | None | Cheap alerting path — see RSS Feed |
Next Actions
- Register at
developer.njtransit.com; confirm current endpoint shapes and rate limits (paths below are from prior knowledge, not verified as of 2026-07-25) - Verify the MyBusNow
.jspendpoints are still live and unauthenticated - Confirm
path.transitdata.nycis still running; decide wrap vs. self-host - Decide stop-identity model — MyBus numeric IDs vs. GTFS
stop_id, and how to map between them - Scaffold the
uvproject +Agencyprotocol with capability flags - GTFS static ingest → SQLite, with a refresh command
- CLI surface:
next <stop>,alerts [--route],stops <query> - Saved stop aliases so
next homeworks - Alert diffing + persistence
- Decide packaging into the flake — see Migrate uvx.nix to uv2nix
Resources
Saved stops (from the 2026-06-20 advisory)
| Stop | MyBus # | Direction |
|---|---|---|
| Palisade Ave at 16th St — home | 21771 | To New York |
| Palisade Ave at 18th St | 21775 | To Jersey City |
| Mountain Rd at 18th St (Troy Towers, 8:11 AM only) | 21855 | To New York |
| Palisade Ave at 3rd St (The Lenox) | 21766 | To New York |
| Palisade Ave at 4th St (The Lenox) | 21780 | To Jersey City |
| JFK Blvd at 5th St — route 125 alternate | 21719 | To New York |
| JFK Blvd at 5th St — route 125 alternate | 21435 | To Journal Square |
| Palisade Ave + Franklin St | 21070 | To Jersey City |
Links
- Travel alerts (route 123):
njtransit.com/travel-alerts-to?tab=Bus&line=123 - MyBusNow web UI:
mybusnow.njtransit.com - GTFS-RT spec:
developers.google.com/transit/gtfs-realtime
Notes
- Bus stop poles carry the 5-digit MyBus number, so stop IDs can be captured in the field.
- NJT bus GTFS is regenerated on the quarterly schedule-change cadence; a stale static cache is exactly how the June 20 breakage propagates into a client. Refresh should be time-based, not lazy.
- The Route 495 PABT delay advisory appears on essentially every PABT-bound route simultaneously. Treat route-wide advisories as a separate display class from trip-level delay — collapsing them is the bug this project exists to avoid.
- NJT’s advisory detail pages and MyBusNow both block automated fetches (bot detection), so scraping HTML is not a viable path. Feeds and APIs only.
Open Questions
- Is the degraded PATH feed worth including in v1 at all, or does it dilute the interface? Arrivals-only may be enough given PATH’s headways.
- Alerting mechanism: poll-and-diff in the CLI, or a background timer that notifies? A timer conflicts with “just local CLI” but is where the actual value is.
- Should
nextfall back from GTFS-RT to scheduled times when nothing is tracking, or report the absence? The 2026-07-25 incident argues for reporting absence explicitly.
Related Projects
- Store API secrets in password manger — NJT token storage
- RSS Feed — advisory feed ingestion overlaps
- Migrate uvx.nix to uv2nix — packaging a Python CLI into the flake