api-server — CONTEXT.md
NestJS 11 backend with Prisma 6 ORM, PostgreSQL + PostGIS, Redis, JWT/OTP auth, and Socket.IO real-time. For global conventions (branding, Angular patterns, GIS overview), see
/CLAUDE.md.
1. Module Structure Pattern
Each feature module lives in src/<feature>/ and contains:
<feature>/
├── <feature>.module.ts
├── <feature>.controller.ts
├── <feature>.service.ts
├── dto/
│ ├── create-<feature>.dto.ts
│ └── update-<feature>.dto.ts
└── guards/ (if feature-specific)
Feature Modules (16 total)
| Module | Controllers | Services | Notes |
|---|---|---|---|
analytics |
analytics | analytics | Dashboard summary stats |
auth |
auth | auth | JWT + OTP login (admin, driver, passenger) |
boardings |
boardings | boardings | QR check-in/out, earnings, trip boardings |
common |
— | twilio | Shared Twilio SMS/WhatsApp service |
config |
— | — | Config files only (CORS, etc.) |
drivers |
drivers | drivers | CRUD + bank accounts + assignment history |
lines |
lines, passenger-stops | lines | Line/route CRUD + public stops + GTFS |
location |
location | location | GPS logging, proximity checks, monitoring |
passenger |
— | gtfs | GTFS data import/query |
payments |
otp, recharge, withdraw | otp, recharge, withdraw, libelula | Full payment stack (Libelula gateway) |
prisma |
— | prisma | Database client wrapper |
schedules |
schedules | schedules, dispatcher | Trip scheduling + auto-dispatch |
tenant |
tenant | tenant | Multi-tenant CRUD |
trips |
trips, passenger-trips | trips | Trip lifecycle + driver/passenger views |
users |
users | users | Admin user creation + passenger listing |
vehicles |
vehicles | vehicles | Vehicle CRUD + assignment history |
2. Auth Flow
- JWT strategy:
JwtStrategy.validate()returns{ userId, email, role, tenantId } - OTP login:
requestOtp→ saves to DB; skips Twilio whenTWILIO_FORCE_REAL_SEND=false - Dev bypass:
OTP_MASTER_KEY=000000fires before driver status guard - Token payload: includes
driverIdviagenerateTokenResponse() - Downstream services: look up driver by
userId(JWTsub), not email - Passenger auth: separate
POST /auth/passenger/request-otpandverify-otpendpoints; auto-creates Passenger record
Role Enum
enum Role {
SUPER_ADMIN
ADMIN
DRIVER
PASSENGER
}
3. Environment Loading
ConfigModule.forRoot({
envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
isGlobal: true,
})
.env.development takes priority in dev; .env is the fallback.
Key Environment Variables
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET |
Token signing secret |
OTP_MASTER_KEY |
Dev bypass OTP (set 000000 in dev only) |
TWILIO_FORCE_REAL_SEND |
false in dev to skip SMS |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN |
Twilio credentials |
TWILIO_SMS_FROM / TWILIO_WHATSAPP_FROM |
Send-from numbers |
TWILIO_OTP_TEMPLATE_ID |
WhatsApp template for login OTP |
LIBELULA_API_URL / LIBELULA_APPKEY |
Libelula payment gateway |
LIBELULA_CALLBACK_ALLOWED_IPS |
IP allowlist for payment callbacks |
API_URL |
Backend URL injected into frontend builds |
NODE_ENV |
development or production |
ADDITIONAL_ORIGINS |
Extra CORS origins (comma-separated) |
ANALYTICS_KEY |
Shared secret for POST /analytics/events (x-analytics-key header). If unset, the endpoint accepts all keys (dev). Must match analyticsKey in each client app's environment.ts. |
Never commit .env files — only .env.example is allowed in version control.
4. Database Schema (Prisma)
Database name: transit_flow
Provider: postgresql with PostGIS extension
Core Models
| Model | Key Fields | Notes |
|---|---|---|
Tenant |
name, domain, email, phoneNumber, status, isActive | Top-level org (municipality/syndicate) |
Line |
name, colorHex, polyline (String?), tenantId | polyline = Google Encoded Polyline for visual track |
Route |
name, lineId, status | Operational path; has ordered Checkpoints |
Checkpoint |
name, latitude, longitude, location (PostGIS), radiusMeters, orderIndex, routeId | location = geography(Point,4326) |
Stop |
name, latitude, longitude, lineId, orderIndex | Passenger-visible stops |
User |
email, password, role, phoneNumber, otpCode, roles[] | Auth entity; roles[] for multi-role |
Driver |
firstName, lastName, ci, licenseNumber, status, balance, tenantId, userId | balance = Decimal(10,2) earnings |
Vehicle |
plate, model, year, capacity, status, tenantId | M:N with Line; M:N with Driver via VehicleAssignment |
VehicleAssignment |
driverId, vehicleId, isCurrent | Joins drivers to vehicles |
Trip |
vehicleId, driverId, lineId, routeId, status, actualCheckpoints (Json) | actualCheckpoints = historical log, never remove |
TripSchedule |
lineId, driverId, vehicleId, startTime (HH:MM), daysOfWeek (bitmask) | Recurring schedule |
VehicleLocationLog |
vehicleId, lat, lng | Raw GPS log |
Payment/Boarding Models
| Model | Key Fields | Notes |
|---|---|---|
Passenger |
userId, qrToken, balance | balance = Decimal(10,2) wallet |
Boarding |
tripId, passengerId, scanFlow, scannedAt, checkedOutAt | Unique per trip+passenger |
BalanceTransfer |
boardingId, passengerId, driverId, amount | One per boarding |
OtpRecord |
userId, otpHash, operationType, expiresAt, used | Payment OTP (separate from auth) |
Transaction |
passengerId, type, amount, status, libelulaTransactionId | Libelula top-ups |
BankAccount |
driverId, accountNumber, bankName, isDefault | Driver saved accounts |
Withdrawal |
driverId, amount, bankAccount, status | Pending/processed/rejected |
Critical GIS Rules
Line.polyline(String?) = Google Encoded Polyline — visual geometry onlyCheckpoint.location= PostGISgeography(Point,4326)— proximity queries viaST_DWithinRoute.pointswas removed (2026-04-12) — geometry lives onLine.polylineonly- OSRM road snapping is called from the browser, not the backend
5. Key Endpoints (Quick Reference)
| Prefix | Controller | Key Endpoints |
|---|---|---|
/auth |
auth | POST login, POST request-otp, POST verify-otp, POST passenger/*, GET me |
/tenants |
tenant | CRUD + GET :id/lines, POST :id/lines |
/lines |
lines | CRUD + PATCH :id/polyline, route CRUD, vehicle assignment |
/trips |
trips | POST self-dispatch, POST :id/start, POST :id/finish, GET my-trips |
/drivers |
drivers | CRUD + GET me, bank account CRUD |
/vehicles |
vehicles | CRUD + GET available |
/boardings |
boardings | GET my-qr, POST :tripId/checkin, POST :tripId/scan-passenger, earnings |
/location |
location | POST (log GPS), GET monitoring |
/payments/* |
otp, recharge, withdraw | OTP flow, Libelula recharge, driver withdrawal |
/passenger/* |
passenger-stops | GET stops/search, GET stops/nearby, GTFS routes/stops |
/analytics |
analytics | GET summary |
/schedules |
schedules | CRUD + deactivate |
6. Dev Workflow
cd apps/api-server
npm install
npm run start:dev # NestJS watch mode
npx prisma studio # DB GUI at :5555
npx prisma migrate dev # Run migrations
npx prisma db push # Push schema without migration history
npx prisma generate # Regenerate Prisma client after schema changes
7. Key File Paths
| What | Path |
|---|---|
| Prisma schema | prisma/schema.prisma |
| App module | src/app.module.ts |
| Auth module | src/auth/ |
| Lines module | src/lines/ |
| Trips module | src/trips/ |
| Location service | src/location/location.service.ts |
| Boardings module | src/boardings/ |
| Payments module | src/payments/ |
| GTFS service | src/passenger/gtfs.service.ts |
| Twilio service | src/common/twilio.service.ts |
| CORS config | src/config/cors.config.ts |
| JWT strategy | src/auth/jwt.strategy.ts |
Last updated: 2026-06-05.