Skip to main content

Backend (MongoDB monolith)

Which backend this is

This page documents the MongoDB backend on mainbackend/server.py, the generation the shipped mobile app depends on. The web tier runs on a separate Postgres backend; read that page for the two-generation context.

backend/server.py is a single FastAPI application (~8,900 lines) served by uvicorn server:app --workers 2 on Render, Python 3.11. It mounts 10 routers and exposes roughly 130 HTTP routes.

Routers & the API surface

Routes group into these areas. Auth tiers: public (no auth), USER (get_current_user), STORE (get_store_admin), ADMIN (get_admin_user).

FastAPI app

/api/auth — 9 routes

/api products, stores, saved — public reads + USER

/api body-photo + consent — USER

/api try-on — USER

/api events, observations, ML data — USER + ADMIN

/api/admin — 18 routes, ADMIN

/api/brand — STORE self-serve

/api/payments — Stripe Connect

/api/agents — ADMIN ops

/api health + web HTML

Auth — /api/auth

POST /signup, POST /login (5/min), GET /me, PUT /profile, POST /upload-photo (sets the deprecated profile_photo_url), POST /forgot-password (3/hr), POST /reset-password, POST /resend-verification, GET /verify-email.

Catalog, stores, social — mostly public reads

GET /api/products (filters + featured sort), `GET /api/products/{id}`, `GET /api/products/{id}/stats`, GET /api/categories, GET /api/brands, GET /api/colors, GET /api/size-recommendation, `GET /api/similar/{id}` are all public. GET /api/recommendations is USER. White-label store reads live under `/api/store/...` including `GET /api/store/qr/{qr_code}` for in-store scanning. Saved items, look-boards, referrals, brand-follows, drops waitlists, and outfits are USER endpoints under /api.

POST /api/consent/give, GET /api/consent/status, POST /api/consent/revoke; POST /api/body-photo/validate, POST /api/body-photo/upload, GET /api/body-photo, DELETE /api/body-photo, POST /api/body-photo/background, and POST /api/analyze-body-photos (measurements + photo in one call).

Try-on — USER

POST /api/try-on (submit), `GET /api/try-on/status/{job_id}` (poll, ownership-checked), GET /api/try-on/rate-limit, DELETE /api/try-on/cache, DELETE /api/try-on/cache/invalid (ADMIN), and GET/POST /api/user/tryon-history.

Admin, brand, payments, agents

  • Admin (/api/admin, ~18 routes): stats, product/store CRUD, brand analytics, submissions review, render logs, drop releases, plus ops probes (/api/admin/fal-status, /api/admin/queue/health).
  • Brand self-serve (/api/brand): POST /waitlist (public), brand stats and orders (STORE), POST /agreement/sign, product image upload and create.
  • Payments (/api/payments): Stripe Connect onboarding, checkout, orders, and the signed webhook — see Payments.
  • Agents (/api/agents, ADMIN): quality/test/bug reports and a Fashn key probe.
The full route table

The exhaustive endpoint list with per-route auth is maintained in the codebase and in frontend/docs/SERVER_ROUTE_MAP.md / API_SURFACE_MAP.md. This page documents the shape; those are the reference.

Auth model

Auth is PyJWT (import jwt as pyjwt), HS256, 24-hour expiry, secret JWT_SECRET_KEY (the app hard-fails at startup if it is unset). python-jose is not present in the codebase — the CVE rule is satisfied.

Bearer JWT (sub = user id)

get_current_user
decode + MongoDB lookup every request

Consumer endpoints

get_store_admin
is_admin OR is_store_admin

get_admin_user
is_admin == true

Seller portal + payments Connect

Admin + ML dataset writes

  • get_current_user decodes the token and looks the user up in MongoDB on every request (no session cache), rejecting suspended (403) and deleted (401) accounts and setting the Sentry user.
  • get_store_admin additionally allows is_store_admin — used by the seller portal and Stripe Connect endpoints.
  • get_admin_user requires is_admin — used for ML dataset writes, analytics, product/store admin, and seeds. body-references/add is correctly ADMIN-gated.
  • Passwords are bcrypt; reset/verify tokens are SHA-256 hashed at rest. A separate POST /api/admin/login checks ADMIN_PASSWORD.

Rate limiting

Two layers work together — see try-on pipeline for how they gate a render.

  • slowapi per-IP decorators on auth/upload endpoints (login 5/min, signup 10/min, try-on 60/min, forgot-password 3/hr).
  • Upstash Redis per-user counters keyed by UTC window: a daily try-on limit of 25 per user (fail-closed — a Redis outage returns 503, not unlimited), a global daily render cap, a 5-per-10-minute burst guard, an 8-second per-garment cooldown, a 60/min poll limit, body-photo upload 5/hr, and events logging 100/hr.
Documented limit vs code

The tech-stack notes describe "10 try-ons/day", but DAILY_TRYON_LIMIT in code is 25. The code is the source of truth; this is tracked on the tech-debt page as a doc/code reconciliation item.

Payments

Stripe Connect Express marketplace. stripe.api_key is set at import; critically, the app hard-fails at import if STRIPE_SECRET_KEY is set but STRIPE_WEBHOOK_SECRET is unset.

MongoStripeFastAPIBuyerMongoStripeFastAPIBuyerPOST /api/payments/checkout1create pending order2Checkout Session (application_fee + transfer_data)3hosted checkout4POST /api/payments/webhook (signed)5verify signature + idempotency key6mark order paid7
  • Connect onboarding creates an Express account, returns an onboarding link, syncs live status, and can produce a dashboard login link or disconnect.
  • Plan-based fees (brand ladder): Freemium/free 10%, Basic/brand 8%, Premium/brand_plus 6%, Grandfathered 5% (admin-only, time-boxed). Seller plans remain seller_power 10%, seller_pro 8%. When a store is Connect-enabled, funds route to the brand via application_fee_amount + transfer_data.destination; otherwise the charge goes to the platform for manual payout. Effective plan (and fee) respects subscription status and Grandfathered end dates via helpers/plan_enforcement.py.
  • Webhook always verifies the signature via construct_event, dedupes with a 7-day Redis idempotency key, and handles checkout.session.completed, account.updated, charge.refunded, checkout.session.expired, and charge.dispute.created. Guest checkout is allowed via an optional inline token decode.
  • Account deletion (DELETE /api/account) runs a 12-step ordered teardown: it deletes Supabase body-photo files, nulls biometric fields and revokes consent, deletes user-generated collections, anonymizes orders (kept for 7-year IRS retention rather than deleted), and writes a two-phase deletion-log audit before removing the user document.

Environment & feature flags

The backend validates its environment on boot (backend/scripts/validate_env.py). Critical vars hard-fail if unset: JWT_SECRET_KEY (32+ chars), ADMIN_PASSWORD, MONGO_URL, the two UPSTASH_REDIS_REST_* values, SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY, FAL_API_KEY, FASHN_API_KEY. In production, missing Fal/Fashn/Supabase also hard-fail. Kill switches include RENDER_ENABLED, FASHN_ENABLED, FLUX2_ENABLED, MARKETPLACE_ENABLED, NEW_USER_SIGNUP_ENABLED, BETA_INVITE_REQUIRED, and QUEUE_MODE.