NJ Transit & PATH Realtime CLI

Objective

A local CLI that answers three questions from the terminal, without opening a trip planner:

  1. When is the next bus/train at a saved stop?
  2. Is it actually delayed, or is that a route-wide advisory being painted onto my trip?
  3. 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

CapabilityNJ TransitPATH
GTFS staticYes — developer portal, tokenYes — PANYNJ
Arrival predictions at a stopYesYes
TripUpdatesYesDegraded (dummy trips)
VehiclePositionsYesNo
Service alertsYesNo 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-bindings for protobuf, pydantic for the domain model, httpx, typer for the CLI.
  • Agency protocol with explicit capability flags; NJTransit and Path adapters 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 .env in 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

SourceEndpointAuthNotes
NJT developer portaldeveloper.njtransit.comTokenReplaced 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=NoneUndocumented, XML. Also getBusesForRoute.jsp, getRoutePoints.jsp. Can change without notice
PATH GTFS-RT bridgepath.transitdata.nyc/gtfsrtNoneThird-party, 5s refresh, degraded (see above)
PATH upstreamRidePATH path-data API / PANYNJ JSON APINoneWhat the bridge wraps. Self-hosting the bridge is an option
NJT advisories RSSnjtransit.com/rss/BusAdvisories_feed.xmlNoneCheap 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 .jsp endpoints are still live and unauthenticated
  • Confirm path.transitdata.nyc is 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 uv project + Agency protocol with capability flags
  • GTFS static ingest → SQLite, with a refresh command
  • CLI surface: next <stop>, alerts [--route], stops <query>
  • Saved stop aliases so next home works
  • Alert diffing + persistence
  • Decide packaging into the flake — see Migrate uvx.nix to uv2nix

Resources

Saved stops (from the 2026-06-20 advisory)

StopMyBus #Direction
Palisade Ave at 16th St — home21771To New York
Palisade Ave at 18th St21775To Jersey City
Mountain Rd at 18th St (Troy Towers, 8:11 AM only)21855To New York
Palisade Ave at 3rd St (The Lenox)21766To New York
Palisade Ave at 4th St (The Lenox)21780To Jersey City
JFK Blvd at 5th St — route 125 alternate21719To New York
JFK Blvd at 5th St — route 125 alternate21435To Journal Square
Palisade Ave + Franklin St21070To Jersey City
  • 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 next fall 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.