A civic-grade, real-time transit platform built for Ashland, Ohio. Rider mobile app, dispatcher & driver web portals, and a live Socket.IO-powered Express + MongoDB backend — all in one monorepo.
Originally developed as an Ashland University URCA senior thesis project, this system is now a full production-style demo of how a small city can run a reliable, accessible, on-demand transit service with live visibility for riders, drivers, and dispatch.
- What's inside
- Architecture
- Feature highlights
- Theming (light / dark / system)
- APT Assist — in-app AI chatbot
- Brand system
- Tech stack
- Quick start
- Environment variables
- Scripts
- Roles & permissions
- Real-time events
- API overview
- Fare calculator
- Repository layout
- Troubleshooting
- License
| Package | Description | Default URL |
|---|---|---|
server |
Express + Mongoose API and Socket.IO real-time layer. | http://localhost:5000 |
client |
React 19 web app — public site, dispatcher console, fleet, driver, admin. | http://localhost:3000 |
mobile |
Expo React Native rider app — booking, live tracking, QR boarding, profile. | Metro (Expo CLI) |
ai-core |
Experimental AI/analytics side-projects (not required to run the platform). | n/a |
┌────────────────────────────┐ ┌────────────────────────────┐
│ mobile (Expo / RN) │ │ client (React 19 / CRA) │
│ • Rider booking │ │ • Public landing site │
│ • Live tracking + QR │ │ • Dispatcher console │
│ • Push-style notifications│ │ • Fleet manager │
│ • Profile / history │ │ • Driver portal + navmap │
└────────────┬───────────────┘ └────────────┬───────────────┘
│ REST + Socket.IO │ REST + Socket.IO
└──────────────────┬──────────────────┘
│
┌───────────▼───────────┐
│ server │
│ Express 5 · Node 18+ │
│ JWT · bcrypt · CORS │
│ Socket.IO 4 │
│ Stripe (scaffolded) │
│ AutoCancelService │
└───────────┬───────────┘
│ Mongoose
┌───────────▼───────────┐
│ MongoDB │
│ Users · Vehicles │
│ Rides · AuditLog │
└───────────────────────┘
- Google Places-powered pickup & drop-off autocomplete.
- Dynamic fare estimation with scheduled vs same-day surcharge, rider
type (general / elderly / disabled / child / companion), and no-show
policy pulled from
server/utils/fareCalculator.js. - Live ride tracking with driver GPS, ETA, and socket-pushed status transitions.
- QR-code boarding pass + ride history.
- APT Assist — in-app chatbot with FAQ answers and deep-links into Book / Track / Rides / Fares screens.
- Light / Dark / System theme with secure-stored preference and OS-change listeners.
- Complete profile: edit info, change password, saved places, payment methods (scaffolded), notifications, accessibility preferences.
- Live fleet map with every vehicle plotted in real time.
- Command center: pending bookings, in-progress rides, auto-cancel sweep for abandoned rides.
- Rider 360 and Driver 360 modals — full lifecycle view, notes, contact, audit trail.
- Broadcast center & walkie-talkie-style chat with individual drivers.
- Fleet manager (add/edit/archive vehicles, assign drivers).
- Driver portal with active manifest, next stop, turn-by-turn nav map.
- Live GPS publishing, status toggles (on-shift / break / off-shift).
- Push-broadcast receiver + walkie channel.
- Profile and password management.
- Cinematic landing page with bounded 3D hero stage (
@react-three/fiber). - Auto day/night palette — the 3D scene shifts between light-sky and dark-city based on the viewer's local hour (06:00–18:59 = day).
- Light / Dark / System theme toggle in the navbar, applied to the entire public surface including a brand-blue bus marquee in light mode.
- Marketing pages: About, Services, Fares (sourced live from the fare calculator), Accessibility, FAQ, Contact.
- Dual staff portal menu: Dispatcher and Driver login targets.
Both the public web site and the rider mobile app ship with a first-class light / dark / system theme system. Every user-facing surface adapts: backgrounds, typography, cards, modals, status bars, marquee artwork, and the login hero gradient.
- Class-based Tailwind dark mode (
darkMode: 'class'intailwind.config.js). - Global
ThemeContextatclient/src/context/ThemeContext.jspersists the user's preference inlocalStorage, listens for OS-level changes viawindow.matchMedia('(prefers-color-scheme: dark)'), and applies thedarkclass on<html>. - A tiny inline script in
client/public/index.htmlapplies the resolved theme before React mounts, eliminating FOUC. - The landing-page bus marquee swaps palettes automatically — Ashland brand-blue in light mode, dark steel in dark mode — while the separate 3D hero keeps its own time-of-day palette.
Theme scope on the web is the public surface only: landing, marketing pages, navbar, footer, book, track, and the login modal. The dispatcher / driver / fleet consoles intentionally stay on the existing operator palette.
- Global
ThemeContextatmobile/context/ThemeContext.jswith three modes:light,dark,system. - Preference persists via
expo-secure-store; OS changes are observed throughAppearance.addChangeListener. - Semantic color tokens in
mobile/constants/theme.jspower amakeStyles(colors)pattern — each screen receives a styles factory so colors stay in sync. - Root background is pushed to the native layer via
expo-system-uiand the status bar style tracks the resolved theme throughexpo-status-bar. - Settings screen exposes a three-button Appearance picker (Light / Dark / System).
The rider mobile app includes APT Assist, a lightweight in-app assistant for FAQs and quick navigation.
- Surface: floating chat bubble on core rider screens + full-screen chat view.
- Engine: rule-based intent classifier in
mobile/utils/chatbotEngine.js, reading from a curated knowledge base atmobile/constants/chatbotKnowledge.js. Keyword scoring + regex matching produce deterministic, offline-capable answers with zero external API cost. - Quick actions: replies can deep-link into the app — e.g. "Book a ride," "Track my ride," "See fares," "Call dispatch" — by dispatching navigation events back into the screen stack.
- LLM-ready:
getReply()is async with a simulated typing delay, so swapping in a server-side LLM endpoint later is a drop-in change.
Files:
mobile/constants/chatbotKnowledge.js # FAQs, intents, actions, fallback
mobile/utils/chatbotEngine.js # Pure classifier (no side-effects)
mobile/screens/ChatbotScreen.js # Full-screen chat UI
mobile/components/ChatbotBubble.js # Floating FAB + pulsing glow
A single brand mark is used everywhere — tab favicon, web navbar / footer, mobile auth screen, and mobile chatbot UI.
- Master SVGs —
client/public/logo.svg(square mark, blue→indigo gradient tile with white bus silhouette) andclient/public/logo-wordmark.svg(horizontal lockup). - Web component —
client/src/components/BrandLogo.jsprovides a single reusable<BrandLogo />withsize,showWordmark,animate, andtone(auto/onDark/onLight) props. - Mobile component —
mobile/components/BrandLogo.jsmirrors the web component usingexpo-linear-gradient+@expo/vector-iconsso the mark is crisp on any device density. - Favicon / PWA manifest —
client/public/index.htmlandclient/public/manifest.jsonreference the SVG mark exclusively; legacy React CRA favicon assets have been removed.
| Layer | Libraries |
|---|---|
| Backend | Node 18+, Express 5, Mongoose 9, Socket.IO 4, JWT, bcrypt, helmet, express-rate-limit, express-mongo-sanitize, xss-clean, morgan, Stripe (scaffold) |
| Web client | React 19, React Router 7, Tailwind CSS 3 (class-based dark mode), Framer Motion 12, Three.js + @react-three/fiber + drei, Recharts, Leaflet, @react-google-maps/api, Axios |
| Mobile client | Expo 54, React Native 0.81, Expo Router 6, react-native-reanimated 4, react-native-maps, expo-location, expo-haptics, expo-linear-gradient, expo-secure-store, expo-system-ui, @expo/vector-icons, react-native-qrcode-svg |
| Tooling | Create React App (client), Expo CLI (mobile), ESLint, PostCSS, Capacitor (Android wrapper scaffold) |
Prereqs: Node.js 18+, a running MongoDB (local or Atlas), and a Google Maps API key with Places API (New), Maps SDK for Android, and Maps SDK for iOS enabled.
# 1. Server
cd server
npm install
cp .env.example .env # then edit (see Environment variables)
npm run dev
# 2. Web client (in a new terminal)
cd client
npm install
npm start # → http://localhost:3000
# 3. Mobile (in a new terminal)
cd mobile
npm install
npm start # Expo CLI will show a QR code for device previewSeed an admin and a test user once the server is up:
cd server
node seed.js # seeds demo vehicles / config
node testAdmin.js # creates a default admin account
node testUsers.js # creates example rider accountsPORT=5000
NODE_ENV=development
MONGO_URI=mongodb://localhost:27017/ashland-transit
JWT_SECRET=replace-with-a-long-random-string
# Optional: Stripe (payments scaffolded, disabled by default)
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=your_google_maps_key
EXPO_PUBLIC_API_URL=http://localhost:5000/api
EXPO_PUBLIC_SOCKET_URL=http://localhost:5000CRA auto-proxies during development. For production, set:
REACT_APP_API_URL=https://your-api.example.com/api
REACT_APP_SOCKET_URL=https://your-api.example.comRun from each package directory.
| Package | Command | Purpose |
|---|---|---|
server |
npm run dev |
Node --watch hot-reload (port 5000). |
server |
npm start |
Production server. |
server |
node seed.js |
Seed demo vehicles. |
server |
node testAdmin.js |
Create / reset the default admin. |
server |
node resetAdmin.js |
Reset admin credentials interactively. |
client |
npm start |
CRA dev server (port 3000). |
client |
npm run build |
Production bundle → client/build/. |
client |
npm test |
React Testing Library / Jest. |
mobile |
npm start |
Expo dev server + Metro. |
mobile |
npm run android |
Launch on Android emulator/device. |
mobile |
npm run ios |
Launch on iOS simulator. |
mobile |
npm run lint |
expo lint. |
Authentication is JWT-based. A single User document has a role field:
| Role | Web portals | Mobile app |
|---|---|---|
rider |
— | ✅ |
driver |
Driver portal (/driver) |
— |
dispatcher |
Dispatcher (/dashboard), Fleet (/fleet) |
— |
admin |
All web portals + destructive admin actions | — |
Server middleware (server/middleware/authMiddleware.js) exposes:
protect— require any authenticated user.requireDispatcherOrAdminrequireDriverrequireAdmin
The server uses named rooms for dispatch and driver targeting. See
server/services/SocketService.js for the authoritative list. Key events:
| Direction | Event | Payload summary |
|---|---|---|
| client → server | driver:location |
{ driverId, lat, lng, heading, speed } |
| client → server | driver:status |
{ driverId, status } (on-shift / break / off) |
| client → server | dispatch:message |
{ toDriverId, text } (walkie / broadcast) |
| server → client | ride:updated |
Ride status transitions (dispatch, rider, driver). |
| server → client | fleet:snapshot |
Periodic active-vehicle broadcast for the public site. |
| server → client | driver:manifest |
Updated manifest for a driver's next stops. |
| server → client | dispatch:broadcast |
Message pushed to all drivers or a specific driver. |
Base URL: http://localhost:5000/api. Full route definitions live in
server/routes/authRoutes.js and server/routes/rideRoutes.js.
POST /auth/registerPOST /auth/loginPOST /auth/forgot-passwordPOST /auth/reset-passwordGET /auth/me(auth)PATCH /auth/profile(auth)PATCH /auth/change-password(auth)
POST /rides— create rider booking.GET /rides/my-rides(auth) — rider history.GET /rides/:id— fetch ride.PATCH /rides/:id/status— update status (driver / dispatch).PATCH /rides/:id/vehicle— assign vehicle.POST /rides/estimate-fare— open; returns a fare breakdown.POST /rides/check-capacity— open; future-dated seat availability.GET /rides/track/:id— open; public tracking payload.GET /rides/fare-info— open; fare policy for marketing site.
GET /rides/fleet/live— open; public snapshot for landing-page HUD.POST /rides/fleet/driver-ping— open; GPS fallback endpoint.- Dispatcher- and admin-only endpoints for vehicles, drivers, broadcast,
rider 360, driver 360 are guarded by
requireDispatcherOrAdmin.
The public "open" endpoints above are exempt from the 200-req/15-min
rate limiter configured in server/index.js.
server/utils/fareCalculator.js is the single source of truth for:
- Rate tables (General, Elderly/Disabled, Child w/wo adult, Companion).
- Scheduled vs same-day differential.
- No-show fee.
- Service hours (Mon–Sat 06:00–21:00).
Both the mobile Fare Info screen and the web Fares page read from it so
published rates can never drift from the fares actually charged at
booking time. Internal tests live in server/utils/__fareTest.js.
.
├─ ai-core/ # Experimental AI/analytics prototypes
├─ client/ # React web app (CRA)
│ ├─ public/ # logo.svg, logo-wordmark.svg, manifest.json
│ └─ src/
│ ├─ components/ # DispatcherDashboard, DriverView, FleetManager,
│ │ # LandingPage, Hero3D, MarketingPages, BrandLogo,
│ │ # ThemeToggle, etc.
│ └─ context/ # ThemeContext (light / dark / system)
├─ mobile/ # Expo React Native rider app
│ ├─ app/ # Expo Router entry
│ ├─ screens/ # Rider flows (booking, tracking, profile,
│ │ # ChatbotScreen, …)
│ ├─ components/ # HeroCanvas, PlacesInput, BrandLogo,
│ │ # ChatbotBubble, …
│ ├─ constants/ # theme.js (semantic tokens),
│ │ # chatbotKnowledge.js
│ ├─ context/ # ThemeContext
│ └─ utils/ # chatbotEngine.js
├─ server/
│ ├─ config/db.js # Mongoose connection
│ ├─ controllers/ # authController, rideController
│ ├─ middleware/ # authMiddleware (JWT + role gates)
│ ├─ models/ # User, Vehicle, Ride, AuditLog, SystemSetting
│ ├─ routes/ # authRoutes, rideRoutes (all HTTP routes)
│ ├─ services/ # SocketService, AutoCancelService, SchedulingService
│ ├─ utils/fareCalculator.js
│ └─ index.js # App entry (Express + Socket.IO)
├─ AGENTS.md # Agent/contributor conventions
├─ TECHNICAL_SUMMARY.md # URCA thesis deep-dive
└─ README.md # You are here
Your MONGO_URI is unreachable or the DB isn't running. Start MongoDB
locally (mongod) or paste a valid Atlas connection string into
server/.env.
- Allow the browser's location permission for the driver page.
- Make sure OS-level location services are on (Windows / macOS).
- The portal must be served over
localhostor HTTPS — plainhttp://on a LAN IP will be rejected by modern browsers.
Set EXPO_PUBLIC_API_URL and EXPO_PUBLIC_SOCKET_URL to your dev
machine's LAN IP (e.g. http://192.168.1.42:5000). localhost inside
an emulator or physical device points at the device itself.
Delete client/node_modules and client/package-lock.json, then
npm install fresh. Three.js + @react-three/* peer versions in
particular are strict about matching.
The scene auto-selects its palette from new Date().getHours(). If you
want to force a mode for a demo, edit client/src/components/LandingPage.js
and pass <Hero3D mode="day" /> or <Hero3D mode="night" />. The default
mode="auto" re-checks every 60 seconds.
This project is provided as-is for educational and research purposes
under the URCA senior thesis program at Ashland University. See
TECHNICAL_SUMMARY.md for the original thesis scope and methodology.