Skip to content

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.

ServiceRoleRequired in prodDegrades to (locally)
Supabase (Postgres)Primary datastoreYes— (required)
Upstash (Redis)BullMQ queues + rate limitsYesIn-memory (per-process)
Cloudflare R2Image/file storageYesUpload paths inert
MSG91SMS (OTP + notifications)Yes (4 templates boot-validated)SMS skipped; in-app still fires
FirebaseOTP verification fallbackOptional—
MeilisearchBatch search indexOptionalPostgres pg_trgm
OpenWeatherMapDashboard weatherOptionalWeather omitted
SentryError tracking / tracingRecommendedInert (empty DSN)

Supabase — PostgreSQL ​

The single primary datastore for the trading platform (the marketing site uses a separate Payload content DB).

  1. Create a Supabase project; note the region (co-locate with the Railway region to keep query latency low).
  2. Grab two connection strings from Project → Database → Connection string:
    • Pooled (transaction pooler / PgBouncer, port 6543) → DATABASE_URL
    • Direct (session, port 5432) → DIRECT_URL (used by prisma migrate deploy)
  3. Set both in the Railway api service.
  4. Tune the pool: DATABASE_POOL_SIZE ≈ (tier connection cap × 0.8) / (number of processes) — typically 20–25 per API instance on the transaction pooler.
env
DATABASE_URL=postgresql://postgres:<pwd>@<host>:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://postgres:<pwd>@<host>:5432/postgres

Migrations 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:

env
REDIS_URL=redis://default:<password>@redis.railway.internal:6379

The 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):

bash
redis-cli -u "$REDIS_URL" DEL "rl:login-phone:phone:+919876543210"
# axes: otp | login-phone | login-device | login-ip | search

Cloudflare 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).

  1. Create an R2 bucket (e.g. cropto-uploads).
  2. Create an R2 API token (Access Key ID + Secret).
  3. Enable a public dev URL (pub-xxxx.r2.dev) or a custom domain for serving.
  4. Set on the api service:
env
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.dev

The 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.

  1. Create an MSG91 account; complete DLT registration (mandatory for India).
  2. Register each SMS template on DLT and note its template ID.
  3. Set the API key, sender ID, and template IDs on the api service.
env
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_EXPIRED templates.
  • 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.

  1. Create a Firebase project; enable Phone auth.
  2. Generate a service-account key.
  3. Set on the api service:
env
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.

  1. Deploy a Meilisearch service (Railway, Mumbai region for latency).
  2. Set on the api service:
env
MEILI_HOST=https://<meili-instance>
MEILI_API_KEY=<master-or-search-key>
  1. Build the initial index once:
bash
node --import tsx apps/api/src/scripts/reindex-batches.ts

Ongoing sync happens via the search-sync BullMQ queue + searchSyncWorker.


OpenWeatherMap — dashboard weather ​

Optional. Provides farm-coordinate-based weather on the dashboard (free tier).

env
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.

  1. Create Sentry projects (one per app or one with environments).
  2. Set the DSNs per environment on each service:
env
# 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 ​

  1. Supabase → DATABASE_URL + DIRECT_URL, run migrations.
  2. Upstash Redis → queues + rate limits.
  3. Cloudflare R2 → uploads.
  4. MSG91 (DLT templates) → OTP + notifications.
  5. Sentry (optional but recommended) → observability.
  6. Firebase / Meilisearch / OpenWeatherMap → as needed.
  7. Deploy the api service, then web / admin (their VITE_API_URL points at the api service), then marketing.
  8. Verify GET /api/v1/health is green and data.config reflects the intended flags.

Internal technical documentation — Cropto