API Backend — apps/api
An Express + TypeScript service. The only writer to the primary database. Controllers are thin; services hold all business logic (106 service files).
Folder structure
text
apps/api/src/
├── index.ts Entry point — imports instrumentation first, warms pool, starts workers, listens
├── app.ts Express setup — CORS, helmet, Swagger, Bull Board, health, all route mounts, errorHandler
├── instrumentation.ts Sentry + OpenTelemetry init (must load before anything else)
├── routes/ 30 route files — thin: wire middleware → controller/service
├── controllers/ Request/response only (auth, farm, farmer, trader, warehouse, public)
├── services/ All business logic (106 files, *.service.ts)
├── validators/ Zod schemas for every request body/query
├── middleware/ authenticate, authorize, requireActiveUser, quota, rateLimit, upload, errorHandler
├── workers/ BullMQ consumers: smsWorker, scheduledWorker, searchSyncWorker
├── queues/ Queue definitions + enqueuers: smsQueue, scheduledQueue, searchSyncQueue
├── lib/ Cross-cutting helpers (prisma, r2, redis, msg91, serializers, pdf, gst, giEligibility…)
├── utils/ jwt, password, pagination, membership
├── data/ Static reference data
├── types/ Local TypeScript types
└── scripts/ One-off + maintenance scripts (backfills, reindex, QA seed)Entry point (index.ts → app.ts)
index.ts imports ./instrumentation first (OpenTelemetry must patch modules before they load), then boots the Express app from app.ts and starts the workers.
app.ts builds the middleware stack in order:
trust proxy(Railway sits behind a reverse proxy).- CORS with a custom origin check — exact
FRONTEND_URL/ADMIN_URL,ecropto.com- subdomains, and (non-prod only) any
http://localhost:<port>;*is forbidden.
- subdomains, and (non-prod only) any
helmet()thenexpress.json().- Swagger UI at
/api/docs, spec JSON at/api/docs.json. - Health check at
/api/v1/health— deep-probes Postgres + Redis, echoes non-secret config, returns 503 when degraded. - Bull Board at
/admin/queues(admin-authenticated). - Gated test endpoints at
/api/v1/test(only whenENABLE_TEST_ENDPOINTS=trueand a secret is set). - All
/api/v1/*route mounts. Admin routes are wrapped withauthenticate + authorize('admin'). Sentry.setupExpressErrorHandler(app)then the app'serrorHandler— last.
Route → controller → service pattern
Most V2 routes call services directly (thin route files); the older farmer/trader/farm/warehouse/public/auth surfaces use dedicated controllers in controllers/.
Middleware
| Middleware | Responsibility |
|---|---|
authenticate | Validate the Authorization: Bearer JWT → attach req.user; set Sentry user (id only). Rejects DEACTIVATED users. |
authorize(role) | Role gate — admin, farmer, trader. |
requireActiveUser | profileComplete guard on all write endpoints. |
requireSellQuota / requireBuyQuota | Check + consume the post-axis quota. |
requireProductsV2 | Feature-flag gate for the products V2 surface. |
rateLimit / rateLimitStore | Redis-backed limiters (OTP, login phone/device/IP, search). Fall back to in-memory without Redis. |
upload | multer (memory) → Sharp compress → R2. Validates magic bytes, rejects SVG. |
errorHandler | Central formatter: { success:false, error:{ code, message } } + status; captures 5xx to Sentry. |
The lib/ toolbox (selected)
| File | Purpose |
|---|---|
prisma.ts | Prisma client singleton + pooled URL builder + warm-up |
redis.ts | Upstash Redis client (nullable when unconfigured) |
r2.ts | Cloudflare R2 S3 client |
msg91.ts | MSG91 HTTP client — only called by smsWorker |
leadSerializer.ts | Lead response enrichment + identity masking |
dealRowSerializer.ts / engagementRowSerializer.ts | Deal / engagement row shaping |
serializers/ | Batch + product serializers (owner-only field stripping) |
batchPricing.ts | Weighted-average displayPricePerKg, blended cost |
giEligibility.ts | GI tag eligibility (Mithilanchal districts) |
gst.ts | Nearest-ancestor GST bracket resolution |
leadLifecycle.ts | Lead state-machine rules |
unreadMessages.ts | Batched unread-count (groupBy, no N+1) |
timezone.ts | IST date helpers (quota boundaries, buckets) |
pdf/ + reports/ | Deal-document + report PDF generation |
messaging.realtime.ts | In-process pub/sub for messaging (EventEmitter in Phase 1) |
featureFlags.ts | Runtime feature toggles |
Workers & queues
Three BullMQ queues, consumed by workers running in the API process:
| Queue | Worker | Jobs |
|---|---|---|
sms-notifications | smsWorker.ts | Send SMS via MSG91 (3 retries, backoff) |
scheduled | scheduledWorker.ts | 8 recurring cron jobs (see Jobs & Workers) |
search-sync | searchSyncWorker.ts | Sync batch documents to Meilisearch |
See API Reference for the endpoint catalogue and Data Model for the schema.
