# Backend (MongoDB monolith)

:::note[Which backend this is]
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](./backend-evolution.md); 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`).

```mermaid
graph TB
    App["FastAPI app"]
    App --> Auth["/api/auth — 9 routes"]
    App --> Catalog["/api products, stores, saved — public reads + USER"]
    App --> Body["/api body-photo + consent — USER"]
    App --> TryOn["/api try-on — USER"]
    App --> Analytics["/api events, observations, ML data — USER + ADMIN"]
    App --> Admin["/api/admin — 18 routes, ADMIN"]
    App --> Brand["/api/brand — STORE self-serve"]
    App --> Pay["/api/payments — Stripe Connect"]
    App --> Agents["/api/agents — ADMIN ops"]
    App --> Health["/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`.

### 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](#payments).
- **Agents** (`/api/agents`, ADMIN): quality/test/bug reports and a Fashn key probe.

:::note[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.

```mermaid
graph TB
    Token["Bearer JWT (sub = user id)"]
    Token --> GCU["get_current_user<br/>decode + MongoDB lookup every request"]
    GCU --> Consumer["Consumer endpoints"]
    GCU --> GSA["get_store_admin<br/>is_admin OR is_store_admin"]
    GCU --> GAU["get_admin_user<br/>is_admin == true"]
    GSA --> Portal["Seller portal + payments Connect"]
    GAU --> AdminEP["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](./try-on-pipeline.md#rate-limiting)
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.

:::warning[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](../audit/tech-debt.md) 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**.

```mermaid
sequenceDiagram
    autonumber
    participant Buyer
    participant API as FastAPI
    participant Stripe
    participant Mongo

    Buyer->>API: POST /api/payments/checkout
    API->>Mongo: create pending order
    API->>Stripe: Checkout Session (application_fee + transfer_data)
    Stripe-->>Buyer: hosted checkout
    Stripe->>API: POST /api/payments/webhook (signed)
    API->>API: verify signature + idempotency key
    API->>Mongo: mark order paid
```

- **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`.
