# Database Schema ## Entity Relationship Diagram ```mermaid erDiagram %% Core Authentication auth_users { uuid id PK string email jsonb raw_user_meta_data } %% User Management profiles { uuid id PK,FK "references auth.users" text name text avatar_url text push_token boolean is_premium uuid couple_id FK timestamptz created_at timestamptz updated_at } couples { uuid id PK text invite_code UK timestamptz created_at } %% Content question_packs { uuid id PK text name text description text icon boolean is_premium boolean is_public content_review_status content_status "allowed required for customer access" int sort_order timestamptz created_at } questions { uuid id PK uuid pack_id FK text text text partner_text "for two-part questions" int intensity "1-5" text[] allowed_couple_genders "couple composition filter" text[] target_user_genders "individual user filter" text[] required_props "props/accessories required" content_review_status content_status "allowed required for customer access" timestamptz created_at } %% Couple Pack Selection couple_packs { uuid couple_id PK,FK uuid pack_id PK,FK boolean enabled timestamptz created_at } %% User Interaction responses { uuid id PK uuid user_id FK uuid question_id FK uuid couple_id FK "nullable: null is a sealed solo answer, claimed at pairing" answer_type answer "yes/no/maybe" timestamptz created_at } matches { uuid id PK uuid couple_id FK uuid question_id FK match_type match_type "yes_yes/yes_maybe/maybe_maybe" boolean is_new timestamptz created_at } messages { uuid id PK uuid match_id FK uuid user_id FK text content text media_path timestamptz read_at timestamptz created_at } %% Subscriptions subscriptions { uuid id PK uuid user_id FK text revenuecat_app_user_id text product_id subscription_status status text[] entitlement_ids timestamptz purchased_at timestamptz expires_at text original_transaction_id text store boolean is_sandbox text cancel_reason timestamptz grace_period_expires_at timestamptz created_at timestamptz updated_at } revenuecat_webhook_events { uuid id PK text event_id UK text event_type text app_user_id timestamptz processed_at jsonb payload } %% Feedback feedback { uuid id PK uuid user_id FK feedback_type type "bug/feature_request/general" text title text description text screenshot_url jsonb device_info feedback_status status text admin_notes timestamptz created_at timestamptz updated_at } %% Relationships auth_users ||--|| profiles : "extends" couples ||--o{ profiles : "has members" couples ||--o{ couple_packs : "selects packs" couples ||--o{ responses : "has responses" couples ||--o{ matches : "has matches" question_packs ||--o{ questions : "contains" question_packs ||--o{ couple_packs : "selected by" questions ||--o{ responses : "answered by" questions ||--o{ matches : "generates" profiles ||--o{ responses : "submits" profiles ||--o{ feedback : "submits" profiles ||--o{ subscriptions : "has" matches ||--o{ messages : "has chat" auth_users ||--o{ messages : "sends" ``` ## Enums | Enum | Values | |------|--------| | `answer_type` | `yes`, `no`, `maybe` | | `match_type` | `yes_yes`, `yes_maybe`, `maybe_maybe` | | `feedback_type` | `bug`, `feature_request`, `general` | | `feedback_status` | `new`, `reviewed`, `in_progress`, `resolved`, `closed` | | `subscription_status` | `active`, `cancelled`, `expired`, `billing_issue`, `paused` | | `content_review_status` | `unreviewed`, `allowed`, `archived` | | `content_entity_type` | `categories`, `question_packs`, `questions`, `dare_packs`, `dares` | ## Catalogue review metadata `categories`, `question_packs`, `questions`, `dare_packs`, and `dares` carry `content_status`, `content_review_reason`, `content_reviewed_at`, and `content_reviewed_by`. Insert triggers force new rows to `unreviewed` with empty review metadata even when a caller supplies forged values. Status changes and same-status re-reviews require a non-blank reason, only super-admin users may make an authenticated decision, reviewer identity/time are database-managed, and visible content edits reset an unchanged decision to `unreviewed`. `content_reviews` is the append-only status-change history. Only super-admins may read it through the authenticated API. Customer catalogue RLS and the legacy recommendation/teaser RPCs fail closed: a category, pack, and question or dare must all be `allowed`. `unreviewed` and `archived` content remains available to administrators but is not returned to customer clients. Service-role response functions independently reject questions unless both question and pack are allowed. ## Key Flows ### Response & Match Flow 1. User submits response via `submit-response` edge function 2. Response saved to `responses` table (UPSERT on `user_id, question_id`) 3. Function checks for partner's response on same question 4. If both answered positively (not "no"), a `match` is created 5. Match triggers push notification to both partners 6. Match unlocks chat thread in `messages` table Without a couple yet, step 3 has no partner response to check: the response is saved with `couple_id IS NULL` (a sealed answer) and no match is created. See [Solo answering and sealed answers](couple-pairing.md#solo-answering-and-sealed-answers). ### Couple Pairing Flow 1. First user creates couple, gets `invite_code` 2. Second user joins via `manage-couple` edge function with invite code 3. Both profiles' `couple_id` set to same couple 4. Every sealed answer (`couple_id IS NULL`) belonging to either member is claimed into the couple, and matches are computed for every question both members have now answered ### Pack Selection Flow 1. Couples can enable/disable packs via `couple_packs` junction table 2. `get_recommended_questions()` filters by enabled packs and the universal allowed-content boundary 3. Two-part questions show `partner_text` to second responder ### Question Targeting System Questions can be targeted at specific audiences using two complementary filters: | Field | Purpose | Level | Example | |-------|---------|-------|---------| | `allowed_couple_genders` | Couple composition filter | Couple | `['male+female', 'female+female']` | | `target_user_genders` | Individual user filter | User | `['female', 'non-binary']` | **How targeting works:** 1. **Couple filter** (`allowed_couple_genders`): Questions only shown to couples matching the composition (e.g., same-sex specific questions) 2. **User filter** (`target_user_genders`): Controls who sees the question **as initiator** (first responder) - If user's gender matches → they can see and answer first - If user's gender doesn't match → they only see it after partner answers (via `partner_text`) **Example flow:** ``` Question: text: "Have your partner suck your tits" partner_text: "Suck your partner's tits" target_user_genders: ['female'] 1. Female sees "Have your partner suck your tits" → swipes YES 2. Male sees "Suck your partner's tits" (partner_text, unlocked by her answer) 3. Both answered → Match created! ``` This ensures gender-appropriate phrasing while still allowing both partners to respond.