Files
Sauci/docs/couple-pairing.md
2025-12-29 23:10:12 +00:00

6.9 KiB

Couple Pairing System

This document explains how couples are created, joined, and managed in Sauci.

Overview

Couples are the core relationship unit in Sauci. Two users link via an invite code to form a couple, enabling shared question answering and match detection.

States

A user can be in one of three states:

State couple_id partner Description
Unpaired null null Not in any couple
Waiting Set null Created couple, waiting for partner
Paired Set Exists Both partners joined

Edge Functions

manage-couple (POST)

Handles creating and joining couples.

Create Couple (empty body):

// Request
{ }

// Response
{ success: true, couple_id: "uuid", invite_code: "abc12345" }

Join Couple (with invite code):

// Request
{ invite_code: "abc12345" }

// Response
{ success: true, couple_id: "uuid" }

Validations:

  • User cannot already be in a couple
  • Invite code must exist
  • Target couple must have < 2 members

manage-couple (DELETE)

Leaves current couple (sets couple_id to null).

// Response
{ success: true }

delete-relationship (DELETE)

Destructive - Completely deletes the couple and all associated data.

What Gets Deleted:

  1. Chat media from storage (iterates through all match folders)
  2. Both partners' couple_id set to null
  3. Couple record deleted (cascades to):
    • All responses
    • All matches
    • All messages
    • All couple_packs settings

Database Schema

Couple Record

CREATE TABLE public.couples (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    invite_code TEXT UNIQUE DEFAULT substr(md5(random()::text), 1, 8),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Invite Code:

  • 8 character hex string (e.g., a1b2c3d4)
  • Auto-generated on insert
  • Case-insensitive matching (lowercased on lookup)
-- profiles table
couple_id UUID REFERENCES couples(id) ON DELETE SET NULL

When a couple is deleted, both profiles automatically have couple_id set to null (preserving accounts).

Size Enforcement

Database trigger prevents > 2 members per couple:

CREATE TRIGGER enforce_couple_size
BEFORE INSERT OR UPDATE OF couple_id ON profiles
FOR EACH ROW EXECUTE FUNCTION check_couple_size();

-- Raises exception if count >= 2

This prevents race conditions where multiple users try to join simultaneously.

Client Flow

Pairing Screen (app/(app)/pairing.tsx)

Unpaired State:

┌─────────────────────────────┐
│        🔗 Pair Up           │
│                             │
│  ┌───────────────────────┐  │
│  │ Have a code?          │  │
│  │ [Enter invite code]   │  │
│  │ [Join Partner]        │  │
│  └───────────────────────┘  │
│                             │
│         ── or ──            │
│                             │
│     [Create New Code]       │
└─────────────────────────────┘

Waiting State:

┌─────────────────────────────┐
│      ❤️ Partner Code        │
│                             │
│   Share this code with      │
│   your partner              │
│                             │
│   ┌───────────────────┐     │
│   │   ABC12345   📋   │     │
│   └───────────────────┘     │
│                             │
│      [Share Code]           │
│                             │
│  Waiting for partner...     │
│         ⏳                   │
└─────────────────────────────┘

Real-Time Partner Detection

When waiting for partner, the screen listens for updates:

// Polling fallback every 5 seconds
const pollInterval = setInterval(() => {
    fetchCouple();
}, 5000);

// Real-time subscription
const subscription = supabase
    .channel(`couple-${couple.id}`)
    .on("postgres_changes", {
        event: "UPDATE",
        schema: "public",
        table: "profiles",
        filter: `couple_id=eq.${couple.id}`,
    }, async (payload) => {
        if (payload.new.id !== user?.id) {
            await fetchCouple();
        }
    })
    .subscribe();

Auto-Redirect

When partner is detected, user is redirected:

useEffect(() => {
    if (couple && partner) {
        router.replace("/(app)/");
    }
}, [couple, partner]);

Flow Diagrams

Create & Share Flow

User A: Unpaired
    │
    ▼
[Create New Code]
    │
    ▼
POST manage-couple (empty body)
    │
    ├──▶ Create couple record
    │    (generates invite_code)
    │
    ├──▶ Set user A's couple_id
    │
    ▼
User A: Waiting
    │
    ├──▶ Display invite code
    │
    ├──▶ [Share Code] → Native share sheet
    │
    └──▶ Subscribe to realtime + poll

Join Flow

User B: Unpaired
    │
    ▼
[Enter Code] → "abc12345"
    │
    ▼
POST manage-couple { invite_code: "abc12345" }
    │
    ├──▶ Validate code exists
    │
    ├──▶ Check couple has < 2 members
    │
    ├──▶ Set user B's couple_id
    │
    ▼
User B: Paired
    │
    ├──▶ Realtime event fires
    │
    ▼
User A: Receives update → Paired

Delete Relationship Flow

User: Profile → Settings → Delete Relationship
    │
    ▼
Confirmation Dialog
    │
    ▼
DELETE delete-relationship
    │
    ├──▶ List all matches for couple
    │
    ├──▶ Delete chat-media storage files
    │
    ├──▶ Set both profiles.couple_id = null
    │
    ├──▶ DELETE couple record
    │    (cascades to responses, matches, messages, couple_packs)
    │
    ▼
Both users: Unpaired

Cascade Behavior

When a couple is deleted, foreign key cascades clean up:

Table Action
profiles couple_id set to null
responses Rows deleted
matches Rows deleted
messages Rows deleted
couple_packs Rows deleted

Storage (chat-media) is manually cleaned before the cascade.

Error Handling

Error Cause Message
Already paired User tries to create/join while in couple "You are already in a couple"
Invalid code Invite code doesn't exist "Invalid invite code"
Couple full Couple already has 2 members "This couple already has two partners"
DB trigger Race condition caught "A couple can only have 2 members"