Back to samples
Apex Transit logo
Transportation

Apex Transit

Real-time Bay Area transit arrivals over RCS

4 min read

Note: the visuals in this demo recording have since been refreshed with sharper brand assets. The conversation flow is identical to what you'll get from a fresh clone.

What's inside

  • Find nearby stops by sharing location through RCS
  • Real-time arrivals from 511.org StopMonitoring API
  • Coverage across BART, Muni, AC Transit, Caltrain, Golden Gate Transit, more
  • GTFS database imported into local SQLite for stop lookup
  • Recent stops and routes per rider

A Bay Area transit chatbot that runs over RCS. Riders share their location through RCS, see the closest stops across BART, Muni, AC Transit, Caltrain, Golden Gate Transit, and other 511.org agencies, then pull real-time arrivals — all from inside the messages app.

This guide walks you from a fresh clone to a working transit demo on your phone in about 15 minutes (the GTFS import is the slow step).

What you'll build

  • A Pinnacle RCS agent that handles location sharing, button taps, and free-form text in three dedicated handlers
  • A local SQLite GTFS database for nearby-stop lookup across all 511.org agencies
  • Real-time arrivals from the 511.org StopMonitoring API
  • Per-rider recently-viewed stops and routes

Prerequisites

1. Clone and install

Bash
git clone https://github.com/pinnacle-samples/Apex-Transit
cd Apex-Transit
npm install

2. Configure environment

Bash
cp .env.example .env
env
PINNACLE_API_KEY=your_pinnacle_api_key_here
PINNACLE_AGENT_ID=your_agent_id_here
PINNACLE_SIGNING_SECRET=your_pinnacle_signing_secret_here
TEST_MODE=false
PORT=3000
 
# 511.org Open Data
API_511_KEY=your_511_api_key_here
 
# Optional: Mapbox geocoding
MAPBOX_API_KEY=your_mapbox_api_key_here

3. Import the GTFS database

The agent looks up nearby stops from a local SQLite copy of the regional GTFS feed. Import it once:

Bash
npm run update-db

This downloads the latest GTFS feed for every supported agency and writes them to cache/gtfs.db. Re-run whenever you want fresh stop data.

4. Expose your webhook

Bash
ngrok http 3000

5. Connect the webhook

In the Webhooks dashboard:

  1. Add https://<your-tunnel-domain>/webhook
  2. Attach it to your RCS agent
  3. Copy the signing secret into PINNACLE_SIGNING_SECRET

6. Run it

Bash
npm run dev

Send MENU or START to your agent. You'll see Apex Transit's main menu with Stops Near Me, Recently Viewed, and Help buttons. Tap Stops Near Me and share your location — the agent returns the closest stops with live arrivals.

How the pieces fit together

Apex-Transit/
├── server.ts                   # Express bootstrap
├── router.ts                   # /webhook POST — dispatches by message type
├── update-db.sh                # GTFS importer
├── handlers/
│   ├── index.ts                # Re-exports the three handlers below
│   ├── button.ts               # Trigger button handler
│   ├── location.ts             # Location-share handler
│   └── text.ts                 # Free-form text handler
├── cache/
│   ├── gtfsCache.ts            # SQLite reader
│   ├── import-gtfs.ts          # GTFS feed → SQLite importer
│   ├── schema.sql              # Stops / routes / agencies tables
│   └── gtfs.db                 # Generated SQLite DB (after import)
└── lib/
    ├── rcsClient.ts            # PinnacleClient instance
    ├── baseAgent.ts            # Shared send + typing helpers
    ├── typing.ts               # Fire-and-forget typing indicator
    ├── agent.ts                # Agent — recently viewed state + presentation
    └── transit/
        ├── arrivals.ts         # 511.org StopMonitoring fetcher
        ├── nearbyStops.ts      # Geo lookup over GTFS DB
        ├── types.ts            # ArrivalInfo, StopData, AGENCY_NAMES
        └── util.ts             # Distance, formatting helpers

Routing by message type

Unlike the other samples, router.ts doesn't switch on a trigger action — it dispatches by RCS message type:

  • RCS_BUTTON_DATA (trigger) → handleButtonClick
  • RCS_LOCATION_DATAhandleLocation
  • RCS_TEXThandleTextMessage

This makes location sharing a first-class flow rather than a special case inside a giant switch statement.

How nearby-stop lookup works

When a user shares their location, handlers/location.ts calls findNearestStops(lat, lng) which runs a haversine query over the SQLite stops table. For each result, it calls the 511.org StopMonitoring API to fetch the routes that actually serve that stop right now (instead of all routes that could serve it).

The result is a small set of cards with current arrivals — usually 3 to 5 stops, ranked by walking distance.

Customize coverage

AGENCY_NAMES in lib/transit/types.ts is the allowlist of supported agencies. Add a new agency, re-run update-db.sh, and the next location share picks it up.

Going to production

  • Set TEST_MODE=false and submit your agent for carrier approval
  • Move gtfs.db to a managed Postgres or MySQL instance with PostGIS for a real geo index
  • Schedule npm run update-db as a nightly cron so the stop catalog stays fresh
  • Add proactive arrival alerts by storing favorite stops per rider and pushing updates from a worker

Resources

© 2026 Pinnacle Software Development, Inc.