Living snapshot of the system — what apps exist, how they talk to the backend, and where
things live in the repo. Update this panel (and ARCHITECTURE.md) when the
architecture changes.
Apps
- Rider app — Flutter,
apps/rider_app. Google-first auth + phone OTP, simulate mode, EN/MS/中文, dark mode. - Driver app — Flutter,
apps/driver_app. Sign-up, onboarding funnel (incomplete → pending → approved), go online, earnings. - Landing site — React + Vite,
apps/landing. Public marketing page: rider/driver offering, 🇲🇾/🇸🇬 requirements, driver registration form, live-editable name & logo. - Admin dashboard — this page, served by the backend at
/admin. Local-dev only, no auth guard yet.
Backend & integrations
- NestJS + PostgreSQL (TypeORM, schema sync on start; DB auto-created by
ensure-database.ts). - Realtime — Socket.IO gateways
/rider&/driver: live location, offers, ride status. - Payments — Stripe charges + Connect payouts. SMS — Twilio OTP. Google — Sign-In, Maps/Places.
- Driver leads — public
POST /driver-applicationsfrom the landing form →driver_applicationstable (kept separate from the driver-app onboarding funnel shown in the Applications tab).
Design language
Own style, not a clone: indigo accent #4F46E5, wavy headers, pill navigation.
Grab/TADA used only as structural references. The landing site follows the same language.
Docs
ARCHITECTURE.md— living design doc (stack, data model, build status).README.md— repo guide & conventions.apps/landing/README.md— landing setup, brand & copy editing.backend/api/README.md— backend setup & credentials.
Runbook — install, configure, run & build
Everything needed to go from a fresh clone to a running system.
1 · Install & run the server (backend/api)
Prerequisites: Node 20+, npm, Docker (for local Postgres — or use AWS RDS).
cd backend/api cp .env.example .env # then edit — see card 2 docker compose up -d # Postgres 16 (user/pass/db: ridehailing) npm install npm run start:dev # auto-creates the DB, API on :3000
This dashboard: http://localhost:3000/admin. Production:
npm run build then npm run start:prod. Useful extras:
npm run typecheck, npm run db:ensure.
2 · Configure the server (.env)
DATABASE_URL— local docker:postgres://ridehailing:ridehailing@localhost:5432/ridehailing; for RDS also setDATABASE_SSL=true(+ optionalDATABASE_SSL_CA_PATH).JWT_ACCESS_SECRET/JWT_REFRESH_SECRET— generate withopenssl rand -base64 48.SMS_PROVIDER—console(OTP printed to server logs, zero setup) ortwilio+ the threeTWILIO_*vars.GOOGLE_CLIENT_ID_WEB/ANDROID/IOS— OAuth client IDs for Google Sign-In.STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET— optional; payments run with mock data if unset.ADMIN_PASSWORD— protects this dashboard: every/admin/*API call needs thex-admin-tokenheader (the login overlay handles it).CORS_ORIGINS— comma-separated production allowlist; unset = dev-open. Native apps are unaffected (no Origin header).HITCH_MAX_TRIPS_PER_DAY(default 2),PUBLIC_BASE_URL,PORT. Platform commission/flat fee/processor fee/competitor benchmark moved off env vars onto the Platform fees tab (DB-backed, admin-editable).
3 · Landing site (apps/landing)
cd apps/landing npm install npm run dev # :5173, proxies /api → :3000
Production build: VITE_API_BASE_URL=https://api.example.com npm run build →
static dist/ folder, host anywhere. VITE_SIMULATE_BACKEND
(default true) makes the driver form fall back to a clearly-labelled
simulated success when the API is unreachable — set it to false in
production. The on-page booking simulator is pure client-side, always available.
4 · Flutter apps (rider & driver)
cd apps/rider_app # or apps/driver_app cp env.example.json env.json # edit API_BASE_URL etc. flutter pub get flutter run
Config is loaded from env.json at startup (bundled asset — no
--dart-define flags). Set "SIMULATE_BACKEND": "true" there to
run the whole app on mock data with no backend. Google Maps keys also go in
AndroidManifest / AppDelegate — see each app's README.
Complete reference
The full surface of the system — expand a section.
📡 API reference — every endpoint, grouped by audience
Generated from the controllers (kept in sync by hand — re-scan
src/**/*.controller.ts when adding endpoints). Auth column:
public (none), rider JWT / driver JWT
(Bearer token), admin (x-admin-token header).
🗄️ Data model — tables & relationships
Core entities and how they relate (TypeORM, schema-sync in dev). Standalone
config/content tables not shown in the diagram: otp_codes,
pricing_config, promotions, news_posts,
plan_ahead_content, feature_flags, route_cache
(cached Google Directions results).
🎚️ Feature flags — every remote toggle
- Server-enforced in matching (take effect immediately):
driver.offers(master switch — off = rides wait for manual assignment),driver.hitchMode,driver.destinationMode,driver.autoAccept,driver.backToBackTrips. - Driver-app UI gating (picked up at app start):
driver.chat,driver.earnings,driver.payouts,driver.rateRider,driver.tripHistory,driver.support. - Rider-app UI gating:
rider.promotions,rider.news,rider.planAhead,rider.scheduledRides,rider.chat,rider.ratings,rider.support. - Flags seed themselves (all enabled) at boot — see
src/feature-flags/feature-flags.service.ts; toggle them in the Features tab.
📱 Feature matrix — what each app does today
Rider app (Flutter)
- Google + phone-OTP auth; Profile → Connect Google preserves phone accounts; app/Google sign-out
- Booking: Places autocomplete, tier picker, fare estimate, promo codes
- Live tracking (socket + polling), driver chat, ratings
- Scheduled rides, trip history
- Payments: Stripe cards, wallet-style methods
- Promotions, news, plan-ahead content
- Dark mode, EN/MS/中文, simulate mode
- Support requests & feedback threads, account deletion
Driver app (Flutter)
- Google + phone-OTP auth (
/driver-auth); separate role tokens and admin approval; Connect Google + tracking cleanup on logout - Onboarding wizard (full-time / Hitch) + document upload
- Go online/offline, live offers with 15s countdown
- Trip lifecycle: heading → arrived → start → complete
- In-ride chat, rate rider
- Earnings, payouts (Stripe Connect), trip history
- Destination mode, auto-accept, back-to-back queue
- Dark mode, EN/MS/中文, simulate mode
Landing site (React)
- Editable brand (name/tagline/logo) — live or in code
- Rider features section
- Booking simulator (SG/MY, real fare formula)
- Driver offer + 🇲🇾/🇸🇬 requirements
- Driver registration →
driver_applications - Simulate-mode fallback when API is down
Admin (this page)
- Password login (
ADMIN_PASSWORD) - Live ops map (5s refresh) + analytics dashboard
- Rides ops: assign, progress, cancel, refund, chat
- Driver fleet, detail view, suspend, application queue
- Payouts: balances, Stripe Connect transfers, history
- Riders, payment test-cards, OTP log
- Pricing tiers, promotions, home content, news
- Support queue + two-way feedback threads
- Feature flags; this Project reference tab
🚀 Deployment & security checklist (before going public)
- Set a strong
ADMIN_PASSWORD; never expose/adminwithout it. - Development console SMS uses
111111without request throttling. Production and real SMS retain the request limit; configure real SMS before launch. - Replace dev JWT secrets (
openssl rand -base64 48) and rotate any credential that ever appeared in a committed file — check.env.exampleholds placeholders only. - Lock down CORS in
main.ts(currentlyorigin: truefor dev) to your real origins. - Production defaults to
DATABASE_SYNCHRONIZE=false. Keep automatic schema changes disabled on restored data; review migrations before future schema changes. - Use
DATABASE_SSL=true+DATABASE_SSL_CA_PATH(RDS CA bundle) for managed Postgres. - Set
STRIPE_WEBHOOK_SECRETand point a real Stripe webhook atPOST /webhooks/stripe. - Add rate limiting / CAPTCHA to the public
POST /driver-applicationsendpoint before heavy marketing traffic. - Build the landing with
VITE_SIMULATE_BACKEND=falseand a realVITE_API_BASE_URL; hostdist/behind HTTPS. - Move
/uploadsto S3/CDN (local disk today) and put the API behind TLS withPUBLIC_BASE_URLset. - Known deferred items: FCM push, Redis presence, PostGIS geo queries, scheduler for far-future rides (ARCHITECTURE.md §14).
🩺 Troubleshooting
- Every panel says "Could not load" — API is up but DB unreachable: check
DATABASE_URL/ Postgres container. - Login overlay rejects you —
ADMIN_PASSWORDunset or mismatched in.env; restart after editing. - Features tab empty — flags seed at boot; restart the backend once.
- Ride stuck in
requested— no eligible driver online anddriver.offersmay be off; assign manually from Rides or bring a driver online. - Landing form always "succeeds" — that's simulate mode (yellow warning shown); start the backend or build with
VITE_SIMULATE_BACKEND=false. - OTP never arrives —
SMS_PROVIDER=consoleprints codes to the server log instead of texting. - Diagrams below don't render — Mermaid CDN unreachable; connect to the internet once and reopen this tab.
- Stale page after editing admin.html — served with
no-store; hard-refresh clears any bfcache copy.
Flows & documentation
End-to-end flows as they're implemented today. Diagrams are drawn with Mermaid (loaded from CDN on first open — needs internet once) and follow the dashboard theme.
1 · Booking & ride lifecycle
The rider is charged when the ride is requested and refunded on cancel.
Status chain: requested → matched → enRoute → arrived → inProgress → completed
(or cancelled at any point before completion).
2 · Driver matching
Nearest-eligible ranking (haversine). Eligibility honors vehicle tier, Hitch daily-trip
cap, destination mode, auto-accept and back-to-back queueing — all gated by feature flags.
Alongside that one-at-a-time push, the Jobs tab (driver.availableJobs flag)
lets any eligible driver browse and directly claim a still-unmatched ride instead of
waiting for an offer.
3 · Driver onboarding & landing-site leads
Two entry points: the landing site produces leads (driver_applications),
while a real Driver account is created by the driver app and walks the
approval funnel reviewed in the Applications tab.
4 · Authentication (Google-first, then phone)
Google OAuth native credentials and real-device verification are still required; see docs/GOOGLE-SIGN-IN.md. Rider/driver access and refresh tokens are separated; account linking requires an existing authenticated session. Same dual path for riders and drivers: verified email immediately via Google, phone OTP optionally attached to the same account (Bearer token prevents duplicates).
5 · Payments & driver payouts
Stripe throughout: rider charge on request, refund on cancel; per-trip earnings ledger (gross / commission / net) and payouts via Stripe Connect Express.
6 · Realtime channels
Socket.IO namespaces with an in-memory presence map (Redis deferred). REST fallback for location pushes; persisted last-known position for this dashboard and cold-start matching.
7 · Content management (promos, news, home, pricing)
Everything the rider app shows that isn't a ride is edited here and served from the
same API — images upload to local /uploads.
8 · Support & feedback
Support requests are a resolve-only queue; feedback is real two-way messaging with one thread per (rider, topic) — general, payment dispute, or account deletion.
Every ride that's ever touched a payment — charged, refunded, or failed — in one place. Click View for the fare breakdown, refund action, and the rider↔driver message thread for that ride.
In-trip audio recordings, started opt-in by either the rider or the driver — the audio file itself stays on that person's phone (not uploaded here yet). This is just the log of that a recording happened, so it stays visible even after the owner deletes it on their end (soft delete — Restore undoes that).
Simulated fleet — there's no real driver app yet, so assigning a driver to a ride on the Rides tab is how a "match" happens. A driver goes busy when assigned and back to available when that ride ends or is cancelled.
Every driver-app sign-up, including incomplete ones still working through the onboarding wizard (their already-uploaded documents show here too). Review the details and documents, then Approve (they can go online immediately) or Reject with a reason (shown to them in the app; they can fix and resubmit).
Who is online right now and every non-terminal ride, refreshed every 5 seconds while this tab is open. The map (OpenStreetMap, no API key) shows online drivers 🚗 and ride pickups 📍.
Online drivers
Active rides
Launch numbers: totals plus daily volume/GMV for the last 14 days.
What the platform owes each driver (lifetime net earnings − payouts). Pay out records the payout and, when Stripe + the driver's Connect account are set up, sends a real transfer — otherwise it just records it (paid outside Stripe).
Outstanding balances
Payout history
Click Payment on a rider to view their saved cards, or attach a Stripe test mode card for exercising the charge-on-request / refund-on-cancel flow.
What riders are actually charged: baseFare + distance(km) × perKmRate,
floored at minFare, ×peakMultiplier during the peak
window. Only enabled tiers show up in the app. Edit a row and hit Save.
What the platform keeps from a driver's fare, and what's shown on the driver's trip-summary / Earnings screens about it. Fee mode is mutually exclusive — only the active mode's rate is actually charged. The processor fee and "you saved" line are shown/hidden in the app via the driver.showProcessorFeeLine / driver.showSavingsMessage feature flags (Features tab), independent of the rates set here. Changes only apply to trips completed after saving — past trips keep the numbers they were actually charged.
How far a ride offer can go looking for a driver. A driver whose GPS is further than this from the pickup point is never offered that ride, even if nobody closer is online — the ride just keeps searching (and stays claimable from the driver app's Jobs tab) until either a driver within range comes online or the rider cancels. Nearest-driver-first ranking inside that radius is unaffected by this setting. Takes effect on the next matching scan (within ~10s) — no restart needed.
Fare = tier price × time-band multiplier (by day type: weekday / weekend / holiday / holiday eve) × rain surge (manual toggle) × demand surge (manual dial or auto-computed from live driver supply vs. pending requests). Draw a zone on the map to give an area its own rules — a pickup outside every zone uses the Global default rules instead.
Zones
Public holidays
Only the holiday date itself needs adding — the day before it is automatically treated as "holiday eve" everywhere.
Preview / test
Check what multiplier a pickup point would get right now, or at a chosen date/time — without touching a real ride.
Home-screen promo banners. Only active ones show in the app, in sort order (lowest first). Upload a photo (cropped to the banner's shape) or fall back to an emoji + color.
Copy for the Home screen's "Plan ahead" card — tapping it in the app opens a detail page showing "Detail page text" (falls back to the short body if left blank).
Posts shown in the rider app's News tab. Only published posts are visible there — draft one first, then flip it on when ready.
Submitted from the rider app's Profile screen — Contact support and Dispute a payment both land here. No auto-response; follow up with the rider directly (phone/email on file under the Riders tab), then mark Resolve.
Real two-way messaging — general feedback (Settings), payment disputes, and account-deletion requests all land here as separate threads per rider, unlike Support above. Every rider message gets an instant auto-reply (like Contact support), and you can also type a real reply that shows up in the app.
Whether each rider/driver has actually registered a device for push (FCM), and when. A push failing usually means one of: the app never got this far (permission denied, or native Firebase config missing — see the banner below), the registration succeeded but the token is now stale (reinstalled app, cleared data), or delivery itself failed (bad credentials, wrong Firebase project). Use Send test on a row to find out which.
Riders
Drivers
Remote on/off switches for whole features in the rider and driver apps. Apps fetch these at startup; a few (matching/offers, hitch, destination mode, auto-accept, back-to-back) are also enforced server-side, so flipping them here takes effect immediately — no app update needed.