RideKit
Docs/Getting started/Deployment

Deployment

Production deployment for a self-hosted licensee (own server, DB, domain, branded apps — not SaaS, per spec Section 1).

Architecture

            ┌────────────┐      ┌──────────────────┐
  clients → │  reverse   │ ───→ │  backend (NestJS) │ ──→ Postgres+PostGIS
  (apps,    │  proxy/SSL │      │  (1..N instances) │ ──→ Redis (cache,
   web)     └────────────┘      └──────────────────┘      queues, sockets)

Multiple backend instances scale horizontally — the Socket.IO Redis adapter fans realtime events across them, and BullMQ workers share the same Redis. This is what the spec's 50k-driver / 100k-rider concurrency targets rely on.

Build & run with Docker

# Build the backend image (multi-stage; ships a slim non-root runtime)
docker build -t taxi-backend ./backend

# Or run the whole stack (db + redis + backend)
docker compose --profile app up -d

The image:

  • runs as the non-root node user,
  • contains only production dependencies + compiled dist,
  • is ~480 MB.

Production checklist

  • NODE_ENV=production.
  • Strong, unique JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (32+ chars). Boot fails otherwise.
  • CORS_ORIGINS set to your real front-end origins. Boot fails in prod if empty (no reflect-any-origin).
  • STORAGE_DRIVER=s3 with bucket + keys configured.
  • SENTRY_DSN set so 5xx errors are reported.
  • A real SMS provider (Twilio / MSG91) configured in Admin → Settings. The default log provider only writes to the server log — with it, an SOS records the incident and mints the live-trip link but no trusted contact is ever actually texted. The admin safety queue labels such incidents, but the alert did not leave the server.
  • TLS terminated at the reverse proxy (Nginx/Caddy/ALB); HTTPS only.
  • Database backups + PITR configured.
  • Run migrations against the production DB on each release — see below.
  • Restrict the DB role: it should not have TRUNCATE/DROP on ledger tables (immutability is also enforced at the role level).

Running migrations in production

npm run migration:run uses typeorm-ts-node-commonjs, which needs ts-node — a devDependency. The production image ships only production dependencies and the compiled dist, so that script fails there with MODULE_NOT_FOUND. Use the compiled data source instead:

docker compose -f docker-compose.prod.yml exec backend \
  node node_modules/typeorm/cli.js migration:run -d dist/database/data-source.js

npm run migration:run remains correct for local development and CI, where dev dependencies are installed.

CI/CD

/.github/workflows/backend-ci.yml runs on every push/PR touching backend/:

  1. lint (zero warnings) → build → run migrations against a real Postgres+PostGIS service → test,
  2. build the Docker image.

Mobile store builds (rider + driver apps)

Both apps use Expo SDK 54 with dynamic app.config.ts (env-driven) and EAS build profiles in each app's eas.json (development / preview / production). White-label vars (APP_NAME, APP_ICON, bundle IDs, API URLs) are documented in each app's ENV.md and in Installation docs.

Per app (mobile/rider, mobile/driver), from that directory:

# one-time: authenticate + create the EAS project
npx eas login
npx eas init                     # sets EAS_PROJECT_ID

# config-level validation (no native toolchain needed)
npx expo config --type public    # env resolves; prod build throws on localhost URLs
npx expo prebuild --clean        # regenerate native projects (picks up new icons)

# Android — produces an .aab for the Play Store
npx eas build -p android --profile production

# iOS — produces an .ipa for App Store Connect
npx eas build -p ios --profile production

# Submit (or upload the artifact manually)
npx eas submit -p android --profile production
npx eas submit -p ios --profile production

Codemagic (configured — codemagic.yaml at the repo root)

Four workflows ship ready: rider-android, rider-ios, driver-android, driver-ios. Point Codemagic at this repo and they appear automatically.

Create these variable groups in the Codemagic UI, every value marked Secure:

Group Variables
kashvi_api EXPO_PUBLIC_API_URL, EXPO_PUBLIC_SOCKET_URL
maps GOOGLE_MAPS_API_KEY
branding APP_NAME, APP_SCHEME, IOS_BUNDLE_ID, ANDROID_PACKAGE (optional — defaults ship)
google_play GCLOUD_SERVICE_ACCOUNT_CREDENTIALS (Play Console → API access → JSON)
appstore App Store Connect API key — attach it as the kashvi_app_store_key integration

Then upload your Android keystore under Code signing identities and name it kashvi_keystore.

Two things the workflows do that are easy to get wrong by hand:

  1. expo prebuild --clean runs on every build. The ios/ and android/ folders are committed, and any build system uses an existing native project as-is — so without regeneration the build keeps whatever was baked in at commit time, including the placeholder Maps key.
  2. They assert the result. After prebuild, the Android workflows grep the generated manifest for the real Maps key (and, for the driver app, for ACCESS_BACKGROUND_LOCATION / FOREGROUND_SERVICE_LOCATION) and fail the build if it is absent. A missing Maps key otherwise produces a perfectly successful build that renders a blank grey map on every device.

Both store publish steps are intentionally conservative: Play uploads to the internal track as a draft, and iOS goes to TestFlight but not straight to the App Store. Promote from each console once you have looked at the build.

Never commit the real Maps key. This product ships as a source zip. A key baked into android/app/src/main/AndroidManifest.xml would reach every buyer and bill you. The committed manifest carries a placeholder on purpose; the real key is injected by CI at prebuild time and never lands in git.

Supplying the build secrets (do this BEFORE your first production build)

A production build fails fast on purpose if the API/socket URLs are missing, point at localhost or a LAN address, or the Maps key is empty (app.config.ts). That guard is what stops a store binary shipping pointed at a dev machine — so the build cannot succeed until you provide real values.

Do NOT put production values in a .env file: every .env* is git-ignored, and a file on disk is easy to leak. Use EAS secrets, which are injected as env vars at build time. Run these once per app (mobile/rider, then mobile/driver):

npx eas secret:create --scope project --name EXPO_PUBLIC_API_URL    --value "https://api.yourdomain.com/api"
npx eas secret:create --scope project --name EXPO_PUBLIC_SOCKET_URL --value "https://api.yourdomain.com"
npx eas secret:create --scope project --name GOOGLE_MAPS_API_KEY    --value "AIza..."

# White-label identity — only if you are rebranding from the defaults
npx eas secret:create --scope project --name APP_NAME       --value "Your Brand"
npx eas secret:create --scope project --name IOS_BUNDLE_ID  --value "com.yourbrand.rider"
npx eas secret:create --scope project --name ANDROID_PACKAGE --value "com.yourbrand.rider"

npx eas secret:list      # verify before building

If you change any identity value you must re-run npx expo prebuild --clean. The ios/ and android/ folders are committed, and EAS uses an existing native project as-is rather than regenerating it — so without that step the build keeps the old app name and bundle id. The same applies to GOOGLE_MAPS_API_KEY: it is written into android/app/src/main/AndroidManifest.xml at prebuild time, so setting the secret alone leaves Android maps blank.

What you must obtain yourself

These cannot be generated from this repo — they are accounts and keys tied to you as the publisher:

What Where Needed for
Expo account expo.dev eas login, running any cloud build
Apple Developer Program ($99/yr) developer.apple.com iOS builds + App Store submission
Google Play Developer ($25 one-time) play.google.com/console Play Store submission
Android upload keystore eas credentials can generate and store it signing the .aab
Google Maps API key console.cloud.google.com maps in both apps
Play service-account JSON Play Console → API access eas submit -p android

Verified locally so far

The following were built and inspected on a developer machine, so the native projects are known-good before you spend a cloud build:

  • Driver Android debug APK — builds; the generated manifest carries ACCESS_BACKGROUND_LOCATION, FOREGROUND_SERVICE_LOCATION and the Maps key.
  • Rider iOS simulator build — builds as a universal (arm64 + x86_64) binary.

No signed release artifact has been produced, because that needs the credentials above. The remaining risk is therefore credential/store configuration, not code.

Pre-submission checklist:

  • Real, distinct per-app icons/splash (see BRANDING-ASSETS.md) — the placeholder is shared across both apps and stores reject duplicates.
  • Production EXPO_PUBLIC_API_URL / EXPO_PUBLIC_SOCKET_URL (HTTPS, not localhost/LAN — the prod build fails fast otherwise).
  • Google Maps key restricted to the app's Android package + SHA-1 and iOS bundle id.
  • Store listings, privacy policy, and (driver app) background-location justification prepared.

Health & observability

  • GET /api/health — readiness (DB + Redis). Returns 503 if either is down — wire this to your load-balancer / k8s readiness probe.
  • Structured JSON logs (pino) with request-id correlation.
  • Sentry captures 5xx + unhandled errors (and they're always logged locally too, even without a DSN).

Scaling notes

  • Run N stateless backend replicas behind the proxy.
  • Redis and Postgres are the shared state — size them first.
  • BullMQ workers can run in the same process (dev) or as separate worker deployments (prod) consuming the same queues.
source: docs/deployment/README.md (ships identically in the product zip)