# Data model

Drape's primary datastore is **MongoDB** (accessed with `motor` async +
`pymongo`). Roughly two dozen collections are in use; the ones that carry the
product are below. Try-on **jobs are not a collection** — they live in Redis with
a short TTL.

## Core relationships

```mermaid
graph TB
    Users["users"]
    Stores["stores"]
    Products["products"]
    Orders["orders"]
    Saved["saved_items"]
    History["tryon_history"]
    Obs["tryon_observations"]
    Photo["body_photo fields<br/>on the user doc"]

    Stores -->|lists| Products
    Stores -->|fulfills| Orders
    Users -->|places| Orders
    Users -->|saves| Saved
    Users -->|generates| History
    Users -->|rates| Obs
    Users -->|owns| Photo
    Products -->|sold in| Orders
    Products -->|referenced by| Saved
```

## Collections

| Collection | Purpose | Notable fields |
|---|---|---|
| `users` | Accounts + fit profile + body photo | `id`, `email` (unique), `password_hash`, `role`, `is_admin`, `is_store_admin`, consent fields, referral fields, `account_status` |
| `products` | Catalog items | `id`, `store_id`, `garment_style` (drives AI routing), `image_url`, `color_variants[]`, `sizes_available`, `fashn_category` |
| `stores` | Brands / white-label storefronts | `id`, `slug` (unique), `plan_type`, `subscription_status`, Stripe Connect fields, BYOK API keys |
| `orders` | Marketplace orders | `id`, `stripe_session_id` (unique), `amount` / `platform_fee` / `net_amount` (cents), `status`, `product_snapshot` |
| `tryon_history` | Per-user render history | user + product + result references |
| `tryon_observations` | Quality/rating records | mirrored to a Supabase table of the same name |
| `body_references` | ANSUR II / sizing reference data | no PII |
| `render_logs` | Per-render audit trail | one doc per render attempt |
| `deletion_log` | BIPA deletion audit | two-phase `deletion_confirmed` flag |

Others in use include `training_events`, `previews`, `saved_items`,
`saved_looks`, `outfits`, `brand_follows`, `drop_waitlist`, `brand_waitlist`,
`onboarding_submissions`, `reset_tokens`, `verify_tokens`, `invite_codes`, and
`bug_reports`. Indexes are created at startup.

## The body-photo fields

The most important — and most sensitive — data hangs off the **user document**.
These fields are written by the pipeline (not declared in the Pydantic model), and
`body_photo_data` is the **single source of truth for try-on eligibility**.

| Field | Meaning |
|---|---|
| `body_photo_data` | Base64 composited JPEG — what try-on actually uses |
| `body_photo_hash` | 16-char hash; part of the render cache key |
| `body_photo_owner_id` | BIPA ownership stamp; mismatch blocks try-on with 409 |
| `body_photo_original` | Supabase path to the original upload |
| `body_photo_cutout` | Background-removed cutout path |
| `body_photo_mask_upper_url` / `body_photo_mask_lower_url` | EVF-SAM garment masks |
| `body_photo_quality_flags` / `body_photo_needs_review` | Quality signals |

:::warning[Known dual-write debt]
`analyze-body-photos` and `body-photo/upload` both write overlapping body-photo
fields (a dual-write pattern with a `TODO` in the code). Consolidation is
deferred — see [tech debt](../audit/tech-debt.md).
:::

## Why jobs live in Redis, not Mongo

A try-on job is ephemeral coordination state, not a record of truth. It is stored
as `` `job:{job_id}` `` in Redis with a 10-minute to 1-hour TTL, carrying
`{status, user_id}`. The `user_id` is what the status endpoint checks for
ownership. The durable audit of a render is the `render_logs` Mongo document; the
job key is allowed to expire.

## Storage (Supabase)

Body photos and garment images are objects, not documents, so they live in
Supabase Storage — **storage only, no Supabase Auth**. The biometric buckets
(`body-photos-original`, `body-photos-cutout`, `body-photos-mask`) are **private**
and accessed via server-side signed URLs; garment buckets are public. All access
uses the service-role key, so authorization is enforced in FastAPI, not by
Supabase RLS. See [security](./security.md) for the full model.
