Skip to main content

Security & privacy model

Drape stores biometric data (full-body photos) for an AI pipeline. That single fact drives most of the security design. This page is the canonical summary; the enforcement lives in backend/server.py and the audit docs under frontend/docs/.

Authentication

Drape uses its own MongoDB-backed JWT auth — there is no Supabase Auth, and Supabase is used for storage only.

verify bcrypt hash

issue JWT

POST /api/auth/login

MongoDB users

JWT
signed with JWT_SECRET_KEY

Authenticated request

get_current_user()
consumer identity

get_admin_user()
admin / ML dataset writes

  • Library: new JWT code uses PyJWT. python-jose is treated as deprecated because of CVE-2024-33663 / CVE-2024-33664 — no new jwt.decode() calls may be added on it.
  • Two dependencies, two trust levels. get_current_user() authorizes consumer users. get_admin_user() authorizes admin-only operations — ML dataset writes, analytics admin, brand management, and the body-references/add endpoint. Admin endpoints must never fall back to get_current_user().
  • Token validated at load. The app checks isTokenExpired() on launch; an expired token triggers logout and a redirect rather than a screen of failed requests.

Biometric data (BIPA)

A body photo is regulated biometric data. The rules below are architectural invariants, not preferences.

RuleEnforcement
Consent before captureBIPA consent enforced at the backend (HTTP 403 if not consented); gates also in onboarding.tsx, fit-setup.tsx, body-photo.tsx. CONSENT_TEXT is single-sourced in src/constants/legalText.ts.
Photo never comes from the client on try-onuser_photo_data is not accepted in TryOnRequest; the body photo is fetched server-side from MongoDB on every request. This blocks a client from submitting a third party's photo.
One owner per photobody_photo_owner_id is written on all three upload paths; a try-on is blocked with HTTP 409 if the owner does not match the authenticated user.
Self-healing on mismatchGET /api/body-photo clears the fields, fires a Sentry error, and returns has_photo: false if it detects an owner mismatch.
Private storageThe body-photos-original, body-photos-cutout, and body-photos-mask Supabase buckets are private (verified 2026-06-03; migrations 001–003 applied).
Right to deletionDELETE /api/account clears MongoDB + Supabase; surfaced as "Delete Account" in profile.

P0 rules that must never regress

These are confirmed-and-closed vulnerabilities. Every change is checked against them:

  1. Body photo source — never accept user_photo_data from the request body; always fetch server-side.
  2. Try-on status ownershipGET /api/try-on/status/{job_id} verifies the caller owns the job. user_id is stored in the Redis job payload at creation and checked at poll time (fixes the original IDOR).
  3. Stripe webhookSTRIPE_WEBHOOK_SECRET is required in production and hard-fails if unset. The log-and-continue branch is a dev shortcut only.
  4. JWT library — PyJWT only for new code.
  5. Admin endpointsget_admin_user() for all ML dataset writes.
  6. CORS"null" is never added to allowed origins (it would permit file:// requests).
  7. Secrets in git historybackend/.env was committed once (commit 64dceea). JWT_SECRET_KEY and FAL_API_KEY were exposed and both have been rotated and validated. History still contains the old values, so repo read access must not be granted externally without a BFG history purge first.

Authorization & RLS (Layer 8)

Authentication proves identity; authorization controls access. Because all Supabase access is server-side via the service-role key, Supabase RLS is defense-in-depth, not the primary control. The primary control is get_current_user() plus a user_id filter on every MongoDB query touching user data.

The rule that keeps biometric data private

auth.uid() RLS policies do not apply to Drape — service-role requests evaluate auth.uid() as NULL. Policies use a role check (auth.role() = 'service_role') instead. Red flags that must never appear: RLS disabled on any table with a user_id, the service-role key in client code, or a body-photo bucket set to public.

For the full picture see frontend/docs/LAYER_8_RLS_AUDIT.md and AUDIT_SECURITY_PRIVACY.md.