Installation
How to set up the backend for local development. (Production deployment is covered in Deployment.)
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node.js | 20.5+ (LTS 20 recommended) | Pinned via backend/.nvmrc. The app refuses older versions (engines field). |
| PostgreSQL | 14+ with PostGIS | Spatial queries (dispatch, geofencing) need PostGIS. |
| Redis | 7+ | Cache, OTP, BullMQ queues, Socket.IO adapter. |
| Docker | optional | For one-command infra via docker-compose.yml. |
Option A — Docker for infra (recommended)
From the repo root:
docker compose up -d # starts Postgres+PostGIS and Redis
Option B — local Postgres + Redis
Ensure both are running, then create the database:
createdb taxi_platform
PostGIS and the other extensions are enabled automatically by the first
migration — you do not need to CREATE EXTENSION manually.
Backend setup
cd backend
nvm use # picks Node 20 from .nvmrc
npm install
cp .env.example .env # then edit .env (see below)
npm run migration:run # creates schema + seeds RBAC roles
npm run start:dev # http://localhost:3000/api ; Swagger at /docs
Environment configuration
Every option lives in .env.example, fully
commented. The ones you must set:
| Variable | Purpose | Notes |
|---|---|---|
DB_* |
Postgres connection | Match your local/Docker creds. |
REDIS_* |
Redis connection | |
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET |
Token signing | Min 32 chars. Generate with node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))". The app refuses to boot in production with a placeholder value. |
STORAGE_DRIVER |
local (dev) or s3 (prod) |
|
DEFAULT_CURRENCY / DEFAULT_LOCALE / DEFAULT_TIMEZONE / DEFAULT_COUNTRY |
Per-install localization (country-agnostic, spec 1A) |
Provider credentials (all optional — graceful fallback)
Third-party integrations are off by default and degrade gracefully when unconfigured (never crash, never block a ride). Configure them in the admin Providers page (encrypted at rest) or via these env fallbacks:
| Area | Env fallbacks | Unconfigured behaviour |
|---|---|---|
| SMS | TWILIO_* / MSG91_* (+ smsProvider setting) |
OTP logged to console |
Resend/SMTP creds (+ emailProvider setting) |
logged, not sent | |
| Storage (prod) | S3_* / R2 endpoint |
local disk |
| Payouts | RAZORPAYX_* |
manual rail |
| Call masking | TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PROXY_SERVICE_SID (+ callMaskingProvider setting) |
direct dial |
| E-invoicing (per country) | GSP_* (India), ZATCA_*, CFDI_PAC_*, NFSE_* |
deterministic local invoice — see E-INVOICING.md |
White-label / branding (mobile apps)
Each app's app.config.ts reads these (defaults preserved):
| Variable | Purpose |
|---|---|
APP_NAME / APP_SCHEME / IOS_BUNDLE_ID / ANDROID_PACKAGE / EAS_PROJECT_ID |
identity |
APP_ICON / APP_ADAPTIVE_ICON / APP_SPLASH / APP_ICON_BG |
icons & splash — see BRANDING-ASSETS.md |
EXPO_PUBLIC_API_URL / EXPO_PUBLIC_SOCKET_URL / GOOGLE_MAPS_API_KEY |
connectivity (prod build fails fast on localhost/LAN URLs) |
Google Maps API key restriction (Android package + SHA-1, iOS bundle) must be set up in Google Cloud Console per the key you supply.
First-run verification
curl http://localhost:3000/api/health
# {"status":"ok","info":{"database":{"status":"up"},"redis":{"status":"up"}}}
If status is error, the failing dependency (DB or Redis) is shown in
the error object — check that service before anything else.
Migrations
npm run migration:run # apply pending
npm run migration:revert # roll back the last one
npm run migration:generate # scaffold from entity changes
Seeding — creating your first admin
The migrations seed reference data (markets, roles, permissions, vehicle types) but deliberately create no users. A fresh install therefore has nobody who can sign in to the admin panel, so this step is required:
cd backend
npm run migration:run # must come first
npm run seed # creates the first super_admin
The seed prints the exact login details when it finishes, for example:
Log in to the admin panel
Phone +919955000001
Role super_admin
Market IN · INR · Asia/Kolkata
Sign in at the admin panel with that phone number and OTP 000000.
000000 is a development master code that works only while no real SMS
provider is configured; it stops working the moment you connect Twilio/MSG91
in Admin → Settings → Providers, after which the real SMS code is required.
The default phone is derived from your active market's dial code. Override it (and anything else) with environment variables:
| Variable | Purpose |
|---|---|
ADMIN_PHONE |
Admin login phone, E.164. Default: derived from the active market (India → +919955000001) |
ADMIN_EMAIL |
Optional admin email |
ADMIN_NAME |
Display name. Default Platform Admin |
ADMIN_PASSWORD |
Optional — also enables POST /api/auth/login for this account |
npm run seed is idempotent and safe on a production database: it adds no
fake data, and re-running it never duplicates or errors. If the phone already
belongs to an account, that account is granted super_admin (the seed warns
when it promotes an existing user).
Demo data (optional)
npm run seed -- --demo
Adds a believable month of operations on top: 5 drivers with vehicles and approved KYC, 5 riders, 24 rides across every status, ratings, promo redemptions, payouts, and the double-entry ledger history those rides imply — so the dashboard, reports, payouts and driver-earnings screens show real numbers instead of zeros. Everything respects the active market: currency, map center, dial codes, driver/vehicle names and the commission cap all come from your configured country, not from hardcoded defaults.
Demo rides created in the requested state are picked up by the normal
dispatch loop while the backend is running, so they transition to
no_drivers after the offer timeout unless a driver app accepts them.
Cleanup
npm run seed -- --clean-demo # remove everything --demo created
npm run seed -- --clean-junk # remove leftovers from manual testing
npm run seed -- --help # full flag reference
Neither runs as part of a normal seed — nothing is ever deleted implicitly.
--clean-demo removes exactly the rows --demo creates (their ids are
derived from a fixed namespace, so real data can't match) and rebuilds the
affected ledger account balances from the surviving postings. Your admin
account is kept.
--clean-junk deletes placeholder and duplicated vehicle types left over from
manual testing, and clears a support_email whose domain matches neither your
brand nor a normal mailbox provider (a value carried over from another
install). It refuses to delete anything still referenced by real data and
prints every action it takes.
Admin panel
The Next.js 14 operator console. It is a separate app from the backend and runs on its own port.
cd admin
npm install
npm run dev # http://localhost:3200
For production:
npm run build && npm start # also :3200
Pointing it at the backend
The admin talks to the backend over HTTP; set the base URL if the backend isn't on the default:
| Variable | Default | Notes |
|---|---|---|
NEXT_PUBLIC_API_BASE |
http://localhost:3000/api |
Must include the /api suffix. Baked in at build time — changing it requires a rebuild, not just a restart. |
The backend must allow the admin's origin. Add it to CORS_ORIGINS in the
backend environment (comma-separated), e.g.
CORS_ORIGINS=http://localhost:3200,https://admin.yourdomain.com.
Logging in
There is no password: the admin uses the same phone + OTP flow as the apps.
Use the phone printed by npm run seed (see Seeding above) and OTP 000000
while the SMS provider is still the dev log provider. Once a real provider
(Twilio/MSG91) is configured, 000000 stops working and a real SMS is sent —
that switch is automatic, there is no flag to remember.
If the login screen rejects your account, it means the user exists but carries
no admin role or permissions. Re-run npm run seed.
Mobile apps (rider + driver)
Both are Expo SDK 54 apps and need a dev client — they use native modules (maps, background location, push, Razorpay) that Expo Go cannot load.
cd mobile/rider # or mobile/driver
npm install
cp env.example .env.development
Then edit .env.development and set EXPO_PUBLIC_API_URL /
EXPO_PUBLIC_SOCKET_URL to your machine's LAN IP — not localhost, which
on a phone means the phone itself:
ipconfig getifaddr en0 # macOS: your LAN IP
Run it:
npx expo start --dev-client # rider defaults to :8081, driver to :8082
Ports: run the rider on 8081 and the driver on 8082 (--port 8082) so both
can run at once.
Building the dev client
The first run needs a native build (once per machine, per app):
npx expo prebuild # generates ios/ and android/
npx expo run:ios # or: npx expo run:android
GOOGLE_MAPS_API_KEY must be set before prebuild — the key is written
into android/app/src/main/AndroidManifest.xml at generation time, so setting
it afterwards leaves Android maps blank. See each app's ENV.md.
A production build refuses to start if the API/socket URLs are missing, point at localhost or a LAN address, or if the Maps key is empty. That guard is deliberate — it is what stops a store binary shipping pointed at a dev machine.
Ports at a glance
| Service | Port | Start command |
|---|---|---|
| Backend API | 3000 | cd backend && npm run start:dev |
| Admin panel | 3200 | cd admin && npm run dev |
| Rider app (Metro) | 8081 | cd mobile/rider && npx expo start --dev-client |
| Driver app (Metro) | 8082 | cd mobile/driver && npx expo start --dev-client --port 8082 |
| PostgreSQL | 5432 | Docker or local |
| Redis | 6379 | Docker or local |