Engineering docs
Deploying to production (Railway + Vercel)
Last updated Oct 26, 2026 · 6 sections
Two services from one GitHub repo (ownagentlabs-tech/dooh-platform).
1. Backend → Railway
- Railway → New Project → Deploy from GitHub repo → pick
dooh-platform. - Service settings → Root Directory:
backend(Procfile runsalembic upgrade head && uvicorn … --port $PORT). - Add PostgreSQL to the project; Railway injects
DATABASE_URL— confirm the service sees it (postgres:// URLs work; SQLAlchemy accepts them via psycopg2). - Add a Volume mounted at
/dataon the backend service. - Service → Variables:
ENVIRONMENT=productionSECRET_KEY=<openssl rand -hex 32>OPS_PASSWORD=<strong password>(ops user is auto-created on boot as ops@dooh.local)- WhatsApp delivery (wallet OTP + cricket code), once the AiSensy account is ready:
WHATSAPP_PROVIDER=aisensy,AISENSY_API_KEY=<from AiSensy → Manage → API key>, and, only if you named the templates differently,AISENSY_OTP_CAMPAIGN/AISENSY_CRICKET_CAMPAIGN(defaultsfreebee_otp/freebee_cricket_code). Then removeWALLET_OTP_DEV. Until then leaveWHATSAPP_PROVIDERunset (log). - AI name check on the wall (optional):
MODERATION_PROVIDER=anthropicandANTHROPIC_API_KEY=<console.anthropic.com>. Roughly ₹0.01 per new name; cached per name. - AI helpers for advertisers (optional, 2026-10-26):
AI_PROVIDER=anthropicwith the sameANTHROPIC_API_KEYswitches on "Suggest with AI" (headline, slogan, prize titles) and the wrong-name guard in the game editor and the approvals panel. Unset (off) the buttons say "Not set up yet; our team can switch this on". Tuning (defaults are fine):AI_MODEL=claude-haiku-4-5-20251001,AI_TIMEOUT_SECONDS=15,AI_CHECK_NAMES_TIMEOUT_SECONDS=4,AI_COPY_MAX_PER_BUSINESS_PER_HOUR=20,AI_CHECK_NAMES_MAX_PER_BUSINESS_PER_HOUR=300,AI_COPY_CACHE_HOURS=24.ANTHROPIC_API_URLis only for tests or a local stub; leave it unset in production. Cost: a copy request is about 1k tokens on Haiku (well under ₹1), cached a day per identical request. Image enhance needs nothing (Pillow). "Make a short clip" needs ffmpeg (next section); without it the menu item says the server cannot render clips yet. - Billing: leave
PAYMENTS_PROVIDERunset (manual) until there is a Razorpay account; invoices still issue and ops mark bank/cash payments by hand. SetBILLING_SELLER_NAME,BILLING_SELLER_GSTIN,BILLING_SELLER_ADDRESS,BILLING_SELLER_STATE_CODEandBILLING_BANK_DETAILS(details separated by|) before the first real invoice. To switch online payment on:PAYMENTS_PROVIDER=razorpay,RAZORPAY_KEY_ID,RAZORPAY_KEY_SECRET(dashboard → Settings → API keys) andRAZORPAY_WEBHOOK_SECRET(dashboard → Webhooks → addhttps://<api>/v1/webhooks/razorpay, eventpayment.captured). Refunds are manual in the Razorpay dashboard for now. - Email to advertisers (password reset, team invites, notifications):
EMAIL_PROVIDER=resend,RESEND_API_KEY=<from Resend → API keys>,EMAIL_FROM=Freebee <hello@yourdomain.in>(a sender verified in Resend) andWEB_URL=https://<your-app>.vercel.app(links in emails). LeaveEMAIL_PROVIDERunset (log) until Resend is ready; never setEMAIL_DEV_ECHOin production (the backend refuses to boot). The daily messages and the Monday weekly report are sent by the built-in scheduler (below); no cron needed. - Built-in scheduler (2026-10-26):
SCHEDULER_ENABLED(unset = on withENVIRONMENT=production;0turns it off,1turns it on elsewhere). It runs inside the one uvicorn process: bidding close every 5 minutes, daily messages 06:30 IST, coupon reminders 10:00 IST, weekly report Mondays 08:00 IST. Ops see each job's last run on the Today page ("Background jobs", with Run now). Keep the API at ONE replica/worker: a second process would run a second scheduler (a DB lock is a TODO inapp/core/scheduler.py). Remove any Railway cron that called the old run-daily / close-due / run-reminders endpoints (harmless, as they are idempotent, but redundant). CORS_ORIGINS=https://<your-app>.vercel.app(comma-separated if more; the app REFUSES to boot in production with dev defaults or CORS "*" — that is intentional)MEDIA_STORE_DIR=/data/mediaSENTRY_DSN=<backend project DSN>(optional — error tracking; off when unset. Release =RAILWAY_GIT_COMMIT_SHA, environment =ENVIRONMENT; phones/OTPs are scrubbed. Seedocs/UPTIME.md)
- Deploy → note the public URL, e.g.
https://dooh-backend-production.up.railway.app. - Do NOT run
scripts/seed.pyin prod (it creates demo screens + demo advertiser). The ops user is created automatically. Add real venues/zones/screens through /ops in the UI.
2. Web → Vercel
- Vercel → Add New Project → import the repo → Root Directory:
web. - Environment variables (Production):
NEXT_PUBLIC_API_URL=https://<railway-backend-url>PLAYER_BACKEND_URL=https://<railway-backend-url>(server-side, used by the /player proxy)NEXT_PUBLIC_SENTRY_DSN=<web project DSN>(optional — browser + server error tracking; off when unset; no replay, no PII. Seedocs/UPTIME.md)NEXT_PUBLIC_POSTHOG_KEY=<project key>+NEXT_PUBLIC_POSTHOG_HOST=eu|us(optional — product analytics on ourselves; off when unset; anonymous events only. Seedocs/ANALYTICS.md)- (never set
NEXT_PUBLIC_MOCK— it is also hard-disabled in prod builds)
- Deploy →
https://<your-app>.vercel.app. - Update Railway's
CORS_ORIGINSwith the final Vercel URL if it changed.
3. Prod smoke test (10 minutes)
https://<app>.vercel.app/ops→ log in ops@dooh.local + your OPS_PASSWORD.- /ops/screens → Add venue → zone → screen (your real first venue).
- Any tablet/iPad → Safari →
https://<app>.vercel.app/player(real cert — no warning) → pairing code appears → claim it in /ops/pair. - Signup as a test advertiser → upload creative → /ops approve → book today on that screen → watch the player pick it up within ~5 min (schedule refresh) → dashboard fills.
- Vision: on the tablet, 5 taps in a corner → Vision ON → allow camera → watcher counts go live.
Notes
- Media uploads live on the Railway volume; Postgres holds everything else. Enable Railway's Postgres backups.
- Signup is OPEN in this phase (anyone with the link can create an advertiser account) — fine for team testing; gate with invite codes before public launch.
- Player schedule refresh is 300s; creative approval + booking materialization are immediate.
- Local dev is unchanged: SQLite + localhost,
scripts/seed.pyfor demo data. - Bidding for busy windows (2026-10-24):
BIDDING_CLOSE_LEAD_HOURS(default 48) sets when a round closes if ops leave the close time empty. The built-in scheduler closes due rounds every 5 minutes (2026-10-26);POST /v1/ops/bidding/close-duestill works and is idempotent. Ops can also press "Close rounds that are due" on a screen's page. - Mall partner (2026-10-26): migration
mall_partner.PARTNER_API_MAX_PER_MIN(default 60) caps requests per partner API key per minute on/v1/partner/*(in-process, like the other throttles;RATE_LIMITS_ENABLED=0turns it off).MALL_DEFAULT_REVENUE_SHARE_BP(default 3000 = 30%) is the share a NEW venue starts on; ops change it per venue. Mall logos live on the media volume beside creatives. Nothing pays malls automatically: ops record each monthly transfer with "Mark paid" on the venue's Mall partner panel.
ffmpeg on Railway (server-side video conversion, 2026-10-24) — OFF until the founder switches it on
The backend converts .mov/.webm/.mkv/.avi and non-H.264 mp4 uploads to H.264/AAC mp4
(app/services/transcode.py) only when ffmpeg and ffprobe are on the host. The
Railway image built by Nixpacks has neither, so today those uploads are refused with
"Our server cannot convert this format yet; export as H.264 MP4", and the creative card's
Trim / Fit to screen menu items are disabled for video (images still work, via Pillow).
To switch it on, uncomment the block in backend/nixpacks.toml:
[phases.setup]
nixPkgs = ["...", "ffmpeg"]
("..." keeps Nixpacks' auto-detected packages, python included.) Risks: build time grows by
roughly 60-90 s and the image by ~150 MB; each conversion is a few seconds of CPU on the API
dyno (uploads are capped at 50 MB / 10 s so one upload cannot pin it for long). Optional env:
TRANSCODE_ENABLED=0 forces the old refuse-with-reason behaviour even with ffmpeg present;
TRANSCODE_TIMEOUT_SECONDS (default 180) aborts a stuck conversion with a clear 422.
The same switch turns on "Make a short clip from this image" (AI helpers, 2026-10-26): a 6 to 10 s
slow-zoom or pan H.264 clip rendered in 1 to 3 s per clip.
New env for prize fulfilment (defaults are fine): PRIZE_MAX_PER_PHONE_PER_CAMPAIGN_DAY=2,
PRIZE_MAX_PER_PHONE_PER_DAY=5, COUPON_REMINDER_WINDOW_HOURS=24,
AISENSY_REMINDER_CAMPAIGN=freebee_coupon_reminder (approve that template in AiSensy:
"{{1}} from {{2}} expires on {{3}} — show it at the counter before then"). The built-in scheduler
runs the reminders daily at 10:00 IST (2026-10-26); POST /v1/ops/coupons/run-reminders still works.
"{{1}} from {{2}} expires on {{3}} — show it at the counter before then"). Schedule
POST /v1/ops/coupons/run-reminders once a day (Railway cron or any scheduler with an ops token).
Dashboard extras (2026-10-26). Video thumbnails ride on the same ffmpeg switch: with ffmpeg on
the host every video upload (creatives, footage library, screen footage) gets a 480 px JPEG
poster frame next to its file; without it nothing is generated and the portal keeps showing the
video itself. After switching ffmpeg on, run POST /v1/ops/footage/thumbnails/backfill once
(the "Make missing thumbnails" button on ops Footage) to fill in older clips. Migration
extras_weekly_report_waitlist adds users.weekly_report (default on) and the waitlist
table. New env (default is fine): WAITLIST_MAX_PER_IP=10, early-access sign-ups per IP per
hour on the public landing page form.
4. CI, monitoring, analytics
- Every push / PR to
mainruns.github/workflows/ci.yml: backend pytest, webtsc+next build, and the Playwright journey suite against a real backend. No secrets needed. Seedocs/TESTING.md. - Uptime: two free monitors — Railway
/healthzand Vercel/api/health(which also pings the backend). Setup + alert rules indocs/UPTIME.md. - Error tracking (Sentry) and product analytics (PostHog) are off until their env vars are set
(
SENTRY_DSNon Railway;NEXT_PUBLIC_SENTRY_DSN,NEXT_PUBLIC_POSTHOG_KEYon Vercel). Both are configured for zero PII. Seedocs/UPTIME.mdanddocs/ANALYTICS.md.