External Services & Provisioning
Cropto integrates several managed services. Most are optional in local development — the code logs and skips a path when its vars are blank — but each is required in production. This page is a service-by-service provisioning guide. The env vars referenced here are documented in full in Environment Variables.
| Service | Role | Required in prod | Degrades to (locally) |
|---|---|---|---|
| Supabase (Postgres) | Primary datastore | Yes | — (required) |
| Upstash (Redis) | BullMQ queues + rate limits | Yes | In-memory (per-process) |
| Cloudflare R2 | Image/file storage | Yes | Upload paths inert |
| MSG91 | SMS (OTP + notifications) | Yes (4 templates boot-validated) | SMS skipped; in-app still fires |
| Firebase | OTP verification fallback | Optional | — |
| Meilisearch | Batch search index | Optional | Postgres pg_trgm |
| OpenWeatherMap | Dashboard weather | Optional | Weather omitted |
| Sentry | Error tracking / tracing | Recommended | Inert (empty DSN) |
Supabase — PostgreSQL
The single primary datastore for the trading platform (the marketing site uses a separate Payload content DB).
- Create a Supabase project; note the region (co-locate with the Railway region to keep query latency low).
- Grab two connection strings from Project → Database → Connection string:
- Pooled (transaction pooler / PgBouncer, port 6543) →
DATABASE_URL - Direct (session, port 5432) →
DIRECT_URL(used byprisma migrate deploy)
- Pooled (transaction pooler / PgBouncer, port 6543) →
- Set both in the Railway api service.
- Tune the pool:
DATABASE_POOL_SIZE ≈ (tier connection cap × 0.8) / (number of processes)— typically 20–25 per API instance on the transaction pooler.
DATABASE_URL=postgresql://postgres:<pwd>@<host>:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://postgres:<pwd>@<host>:5432/postgresMigrations vs the pooler
prisma migrate dev/deploy needs the direct connection — the pooler closes the DDL connection mid-migration. Some existing migrations were applied via prisma db execute + prisma migrate resolve --applied for this reason.
Redis (BullMQ + rate limits)
Backs the BullMQ queues (SMS, scheduled, search-sync) and the rate-limit counters.
On Railway: add the Redis plugin and reference its private URL from the api service:
REDIS_URL=redis://default:<password>@redis.railway.internal:6379The private endpoint is plain redis:// (no TLS) and free (no egress). The client applies TLS only for a rediss:// URL (e.g. Upstash) and resolves the IPv6 *.railway.internal host via family: 0 automatically. REDIS_URL is preferred; the legacy UPSTASH_REDIS_URL is still read as a fallback.
Without Redis, queues and rate limits fall back to per-process in-memory behaviour: still functional, but not shared across instances and wiped on restart.
Release a locked-out user manually (drop --tls for a plain redis:// URL):
redis-cli -u "$REDIS_URL" DEL "rl:login-phone:phone:+919876543210"
# axes: otp | login-phone | login-device | login-ip | searchCloudflare R2 — file storage
Stores product/batch/farm/warehouse photos and deal-document PDFs. Images are compressed with Sharp before upload (max 1200px width, JPEG q80, target < 500KB).
- Create an R2 bucket (e.g.
cropto-uploads). - Create an R2 API token (Access Key ID + Secret).
- Enable a public dev URL (
pub-xxxx.r2.dev) or a custom domain for serving. - Set on the api service:
R2_ACCOUNT_ID=<cloudflare-account-id>
R2_ACCESS_KEY_ID=<key-id>
R2_SECRET_ACCESS_KEY=<secret>
R2_BUCKET_NAME=cropto-uploads
R2_PUBLIC_URL=https://pub-xxxx.r2.devThe database stores only the object key; the full URL is built from R2_PUBLIC_URL at response time. Uploads verify magic bytes (not just MIME), reject SVG entirely, and never reuse the original filename (keys are generated with nanoid).
MSG91 — SMS
Sends OTPs and transactional notifications. Every SMS event also writes an in-app notification, which is the authoritative channel — SMS is best-effort.
- Create an MSG91 account; complete DLT registration (mandatory for India).
- Register each SMS template on DLT and note its template ID.
- Set the API key, sender ID, and template IDs on the api service.
MSG91_API_KEY=<key>
MSG91_SENDER_ID=CROPTO
MSG91_TEMPLATE_ID_OTP=<id>
MSG91_TEMPLATE_ID_OFFER_ACCEPTED=<id>
# … plus the engagement + optional event templates …- Boot-validated in production (the API refuses to start without them):
OTP,OFFER_ACCEPTED,BID_PLACED,BID_ACCEPTED, and the two*_AUTO_REJECTED_LEAD_EXPIREDtemplates. - Optional templates (order/dispatch/receipt/grievance): if unset, that SMS is skipped; the in-app notification still fires.
- The full catalogue with per-template variable mappings is in SMS Notifications.
SMS is dispatched only by the smsWorker off the sms-notifications queue — never called inline in a request handler.
Firebase — OTP fallback
Firebase Admin SDK verifies phone OTPs as a fallback path (/auth/firebase-verify); the platform then issues its own JWT.
- Create a Firebase project; enable Phone auth.
- Generate a service-account key.
- Set on the api service:
FIREBASE_PROJECT_ID=<id>
FIREBASE_CLIENT_EMAIL=<service-account-email>
FIREBASE_PRIVATE_KEY=<private-key>Meilisearch — batch search index
Optional. When configured, powers product/batch search; otherwise search falls back to Postgres pg_trgm.
- Deploy a Meilisearch service (Railway, Mumbai region for latency).
- Set on the api service:
MEILI_HOST=https://<meili-instance>
MEILI_API_KEY=<master-or-search-key>- Build the initial index once:
node --import tsx apps/api/src/scripts/reindex-batches.tsOngoing sync happens via the search-sync BullMQ queue + searchSyncWorker.
OpenWeatherMap — dashboard weather
Optional. Provides farm-coordinate-based weather on the dashboard (free tier).
OPENWEATHERMAP_API_KEY=<key>Sentry — observability
@sentry/node v10 bundles OpenTelemetry and auto-instruments Express/HTTP/Prisma on the API; @sentry/react covers web + admin.
- Create Sentry projects (one per app or one with environments).
- Set the DSNs per environment on each service:
# api
SENTRY_DSN=<dsn>
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE=<git-sha>
# web / admin
VITE_SENTRY_DSN=<dsn>
VITE_ENV=production
VITE_RELEASE=<git-sha>Leave DSNs empty in dev/CI so Sentry initialises but drops events silently (NODE_ENV=test skips init entirely). Only a DPDP-safe user id is attached to events (Sentry.setUser({ id })) — never phone or other PII.
Provisioning order for a fresh environment
- Supabase →
DATABASE_URL+DIRECT_URL, run migrations. - Upstash Redis → queues + rate limits.
- Cloudflare R2 → uploads.
- MSG91 (DLT templates) → OTP + notifications.
- Sentry (optional but recommended) → observability.
- Firebase / Meilisearch / OpenWeatherMap → as needed.
- Deploy the api service, then web / admin (their
VITE_API_URLpoints at the api service), then marketing. - Verify
GET /api/v1/healthis green anddata.configreflects the intended flags.
