Backend (MongoDB monolith)
This page documents the MongoDB backend on main — backend/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).
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.
Body-photo & consent — USER
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 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.
get_current_userdecodes the token and looks the user up in MongoDB on every request (no session cache), rejectingsuspended(403) anddeleted(401) accounts and setting the Sentry user.get_store_adminadditionally allowsis_store_admin— used by the seller portal and Stripe Connect endpoints.get_admin_userrequiresis_admin— used for ML dataset writes, analytics, product/store admin, and seeds.body-references/addis correctly ADMIN-gated.- Passwords are
bcrypt; reset/verify tokens are SHA-256 hashed at rest. A separatePOST /api/admin/loginchecksADMIN_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.
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.
- 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/
free10%, Basic/brand8%, Premium/brand_plus6%, 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 viaapplication_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 viahelpers/plan_enforcement.py. - Webhook always verifies the signature via
construct_event, dedupes with a 7-day Redis idempotency key, and handlescheckout.session.completed,account.updated,charge.refunded,checkout.session.expired, andcharge.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.