SMART trip is a community-built schedule app for Sonoma-Marin Area Rail Transit (SMART). It runs on the web and ships as Capacitor-based iOS and Android apps.
This project is not affiliated with Sonoma-Marin Area Rail Transit.
- Static SMART train schedules with weekday and weekend service views
- Live trip updates from SMART GTFS-Realtime data, including delays, cancellations, and skipped stops when available
- Live SMART service alerts with unread state stored locally on-device
- Ferry connections for Larkspur trips
- Closest-station selection using device location
- Trip detail sheets with GPS-assisted distance-to-stop messaging and better in-motion progress inference
- English and Spanish UI
- Light, dark, and system theme support
- Fare lookup for supported rider categories
- Shared web, iOS, and Android codebase
- React 18 + TypeScript
- Vite
- Tailwind CSS + Radix UI primitives
- TanStack Query
- React Router
- Capacitor for iOS and Android
- Vercel serverless functions for GTFS-Realtime proxying
- Node.js 18+
- npm
- A 511.org API key for live GTFS data
git clone <repository-url>
cd smart-trip
npm installCreate .env.local with:
TRANSIT_511_API_KEY=your_key_hereFor the full app, including local serverless API routes:
npm run dev-vercelFor frontend-only development:
npm run devIf you use npm run dev, real-time endpoints only work when either:
USE_SAMPLE_DATA=trueis set in.env.local, or- a local API target is running at
http://localhost:3000, or DEV_API_PROXY_TARGETin.env.localpoints/apiat another backend such as a feature-branch deployment
Default local URL:
http://localhost:3210
| Variable | Where | Purpose |
|---|---|---|
TRANSIT_511_API_KEY |
.env.local or Vercel env |
511.org API key for static and realtime feeds |
USE_SAMPLE_DATA |
.env.local |
Serve fixtures from sample/ instead of the live API |
DEV_API_PROXY_TARGET |
.env.local |
Dev-only /api proxy target for npm run dev; defaults to http://localhost:3000 |
VITE_API_BASE_URL |
.env.native or .env.native.local |
Absolute API base URL for native builds |
KV_REST_API_URL / KV_REST_API_TOKEN |
Vercel env (auto-injected) | Upstash Redis credentials for the shared GTFS-RT cache. Created by the Vercel ↔ Upstash integration; if absent, the cache falls back to direct 511 fetches |
For production on Vercel, keep TRANSIT_511_API_KEY server-side only.
npm run dev- Start the Vite dev servernpm run dev-vercel- Start Vercel dev on port 3210npm run build- Type-check and build the web appnpm run build-native- Type-check and build the native web bundlenpm run preview- Preview the production buildnpm run lint- Run ESLintnpm run typecheck- Run TypeScript in build mode without emitting filesnpm run test:unit- Run unit tests with Vitestnpm run update-transit- Refresh GTFS schedule source data from 511.orgnpm run update-build- Refresh transit data, then buildnpm run sync- Build native bundle and sync Capacitor projectsnpm run sync-live- Sync Capacitor projects for live reload developmentnpm run assets- Generate app icons and splash screensnpm run open-ios- Open the iOS project in Xcodenpm run open-android- Open the Android project in Android Studio
Static schedule data is generated into src/data/generated/ and published to public/data/schedules.json during prebuild.
Realtime data flows through Vercel serverless routes, fronted by a shared Redis cache so 511 is polled once per window globally (see below):
511.org GTFS-Realtime
-> Upstash Redis cache (poll-on-read + lock, api/_feedCache.ts)
-> /api/gtfsrt/{alerts,tripupdates,vehiclepositions}
-> React Query hooks
-> trip cards, trip detail sheets, map, and service alerts UI
Trip updates are matched against static schedule entries by scheduled origin departure time because SMART trip IDs are regenerated per service date.
511's Open Data API allows ~370 requests/hour for our token, and explicitly
expects a single central backend to fetch once and fan out to all clients —
the upstream rate must not scale with users. api/_feedCache.ts implements
that: each feed is fetched from 511 at most once per freshness window
(vehicles 15s, trip updates 40s, alerts 5min ≈ 342 calls/hr total), cached in
Upstash Redis, and served to every region/user from that one snapshot. A short
lock prevents concurrent refreshes, and the last-known-good snapshot is served
if 511 is down.
Realtime cache region (TODO: consider moving west). The Redis primary region and the Vercel serverless functions both run in
iad1(Washington, D.C.) — co-located so cache reads/writes stay in-region. Riders and the 511 upstream are in the SF Bay Area, so relocating both the functions (regions: ["sfo1"]) and the Redis primary region to SF would cut latency. Move them together — splitting across coasts adds a cross-country hop to every cache op. Deferred for now; revisit if realtime latency matters.
Native builds use Capacitor and read VITE_API_BASE_URL from .env.native so app requests go to a deployed API base instead of relative web paths.
Use .env.native.local for developer- or branch-specific overrides such as a PR deployment URL. That file is already ignored by Git.
Typical flow:
npm run sync
npm run open-ios
# or
npm run open-androidFor live reload on device or simulator:
npm run dev-vercel
npm run sync-livesync-live uses your Mac's Wi-Fi IP address. The device and development machine need to be on the same network.
Releases are tagged vX.Y.Z and shipped to the App Store and Google Play. The web app deploys continuously from main via Vercel and is not gated by this process.
| Bump | When |
|---|---|
major |
Breaking changes to persisted state, removed features, or otherwise-incompatible behavior. |
minor |
New user-visible features (e.g. departure reminders, station-sheet redesign). |
patch |
Bug fixes, schedule data refreshes, and behind-the-scenes improvements only. |
From a clean checkout of main:
git checkout main
git pull --tags
npm install
npm run test:unit
npm run typecheck
npm run prebuild # should produce zero diff against committed generated filesIf prebuild leaves a diff, land a chore: refresh transit feeds commit before tagging.
npm version runs scripts/version.sh, which updates the native version metadata in lockstep so the three platforms can't drift:
package.json/package-lock.json— semver stringandroid/app/build.gradle—versionNameandversionCode(incremented by 1)ios/App/App.xcodeproj/project.pbxproj—MARKETING_VERSIONandCURRENT_PROJECT_VERSION(build number, incremented by 1)
npm version minor # or `patch` / `major`
git push --follow-tagsThat creates a commit titled e.g. 1.5.0 and pushes the matching v1.5.0 tag.
npm run build-native # prebuild + tsc --noEmit + vite build --mode native
npm run sync # npx cap sync — copies the web bundle into ios/ and android/npm run open-iosIn Xcode:
- Select Any iOS Device (arm64) as the run destination.
- Product → Archive.
- When Organizer opens, Distribute App → App Store Connect → Upload.
- In App Store Connect, create a new version, paste the iOS release notes (see below), and submit for review.
npm run open-androidIn Android Studio:
- Build → Generate Signed App Bundle / APK → Android App Bundle.
- Choose the upload keystore.
- Upload the resulting
.aabin Google Play Console → Production → Create new release, paste the Android release notes, and roll out.
Keep store notes short and user-visible. Skip refactors, build-only changes, web-only SEO work, and platform-specific fixes that don't apply to the store you're publishing to (e.g. don't list an Android-only fix in the App Store notes).
To gather candidates:
git log v<previous>..HEAD --oneline --no-mergesGroup feat(*) first, then user-visible fix(*). Trim anything users won't notice.
- The app can request location permission for closest-station selection and GPS-assisted trip detail messaging.
- Location stays on-device and is not sent as part of schedule or realtime API requests.
- The app stores a small amount of local state such as theme, language, fare preference, and dismissed/read alert state.
- The web app includes Vercel Analytics; native builds do not mount that analytics client.
api/ Vercel serverless realtime proxy routes
public/ Static assets and hosted support/privacy pages
sample/ Local GTFS-Realtime fixtures
scripts/ Feed update and build helper scripts
src/components/ UI components
src/data/ Static and generated schedule data
src/hooks/ App hooks
src/lib/ Utilities and app logic
src/pages/ Route-level pages
src/types/ Shared TypeScript types
ios/ Capacitor iOS project
android/ Capacitor Android project
npm run test:unitnpm run build
npm run lint currently fails in this repo because of pre-existing issues in generated Android build output and unrelated source files.
The code in this repository is available under the MIT License. See LICENSE.
The SMART trip name, logo, and other project branding are not granted under the code license.
Built with ❤️ for the SMART Train community