Freebee Ads docs

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

  1. Railway → New Project → Deploy from GitHub repo → pick dooh-platform.
  2. Service settings → Root Directory: backend (Procfile runs alembic upgrade head && uvicorn … --port $PORT).
  3. Add PostgreSQL to the project; Railway injects DATABASE_URL — confirm the service sees it (postgres:// URLs work; SQLAlchemy accepts them via psycopg2).
  4. Add a Volume mounted at /data on the backend service.
  5. Service → Variables:
    • ENVIRONMENT=production
    • SECRET_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 (defaults freebee_otp / freebee_cricket_code). Then remove WALLET_OTP_DEV. Until then leave WHATSAPP_PROVIDER unset (log).
    • AI name check on the wall (optional): MODERATION_PROVIDER=anthropic and ANTHROPIC_API_KEY=<console.anthropic.com>. Roughly ₹0.01 per new name; cached per name.
    • AI helpers for advertisers (optional, 2026-10-26): AI_PROVIDER=anthropic with the same ANTHROPIC_API_KEY switches 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_URL is 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_PROVIDER unset (manual) until there is a Razorpay account; invoices still issue and ops mark bank/cash payments by hand. Set BILLING_SELLER_NAME, BILLING_SELLER_GSTIN, BILLING_SELLER_ADDRESS, BILLING_SELLER_STATE_CODE and BILLING_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) and RAZORPAY_WEBHOOK_SECRET (dashboard → Webhooks → add https://<api>/v1/webhooks/razorpay, event payment.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) and WEB_URL=https://<your-app>.vercel.app (links in emails). Leave EMAIL_PROVIDER unset (log) until Resend is ready; never set EMAIL_DEV_ECHO in 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 with ENVIRONMENT=production; 0 turns it off, 1 turns 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 in app/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/media
    • SENTRY_DSN=<backend project DSN> (optional — error tracking; off when unset. Release = RAILWAY_GIT_COMMIT_SHA, environment = ENVIRONMENT; phones/OTPs are scrubbed. See docs/UPTIME.md)
  6. Deploy → note the public URL, e.g. https://dooh-backend-production.up.railway.app.
  7. Do NOT run scripts/seed.py in 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

  1. Vercel → Add New Project → import the repo → Root Directory: web.
  2. 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. See docs/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. See docs/ANALYTICS.md)
    • (never set NEXT_PUBLIC_MOCK — it is also hard-disabled in prod builds)
  3. Deploy → https://<your-app>.vercel.app.
  4. Update Railway's CORS_ORIGINS with the final Vercel URL if it changed.

3. Prod smoke test (10 minutes)

  1. https://<app>.vercel.app/ops → log in ops@dooh.local + your OPS_PASSWORD.
  2. /ops/screens → Add venue → zone → screen (your real first venue).
  3. Any tablet/iPad → Safari → https://<app>.vercel.app/player (real cert — no warning) → pairing code appears → claim it in /ops/pair.
  4. 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.
  5. 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.py for 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-due still 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=0 turns 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 main runs .github/workflows/ci.yml: backend pytest, web tsc + next build, and the Playwright journey suite against a real backend. No secrets needed. See docs/TESTING.md.
  • Uptime: two free monitors — Railway /healthz and Vercel /api/health (which also pings the backend). Setup + alert rules in docs/UPTIME.md.
  • Error tracking (Sentry) and product analytics (PostHog) are off until their env vars are set (SENTRY_DSN on Railway; NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_POSTHOG_KEY on Vercel). Both are configured for zero PII. See docs/UPTIME.md and docs/ANALYTICS.md.
Deploy · Docs