Stage 4: FORGE -- Final Creative Architecture
FirstDate is a production-first reality dating series that treats transparency as its format, not its liability. Three asymmetric roles (Host, Applicant, Viewer) orbit a 10-week seasonal arc set in Miami, where every consent ritual, every sponsor deal, every casting decision is designed to be seen. The app is not a dating platform with a show bolted on. It is a show management system whose public membrane happens to look like a dating app. Swiping is a personality quiz, not a match engine. The episode is the produc
Full Public Reader
Stage 4: FORGE -- Final Creative Architecture
1. Core Formula
FirstDate is a production-first reality dating series that treats transparency as its format, not its liability. Three asymmetric roles (Host, Applicant, Viewer) orbit a 10-week seasonal arc set in Miami, where every consent ritual, every sponsor deal, every casting decision is designed to be seen. The app is not a dating platform with a show bolted on. It is a show management system whose public membrane happens to look like a dating app. Swiping is a personality quiz, not a match engine. The episode is the product. The date is the shoot. The restaurant is the set. Everything else serves the content flywheel.
---
2. Role Architecture
Role Enum
-- Replaces is_admin BOOLEAN on fd_profiles
CREATE TYPE fd_role AS ENUM ('host', 'applicant', 'viewer');
ALTER TABLE fd_profiles
ADD COLUMN role fd_role DEFAULT 'viewer',
DROP COLUMN is_admin;
-- Backfill: existing admin → host
UPDATE fd_profiles SET role = 'host' WHERE is_admin = true;// Profile.swift
enum FDRole: String, Codable {
case host
case applicant
case viewer
}
struct Profile: Codable, Identifiable, Equatable {
// ... existing fields ...
var role: FDRole // replaces isAdmin: Bool
// remove: var isAdmin: Bool
}Host (Mohamed)
Tabs: 5 (Inbox, Pipeline, Schedule, Ledger, Studio)
| Tab | Primary View | Actions |
|---|---|---|
| Inbox | ApplicationInboxView | Review, shortlist, select, pass applications. Assign talent to episode. |
| Pipeline | EpisodePipelineView + DealsSection | Kanban episodes across 6 statuses. Manage deals (pitch_stage). View per-episode financials inline. |
| Schedule | ProductionCalendarView | Set shoot dates, detect conflicts, view episode timeline. |
| Ledger | FinancialsView | P&L by episode, breakeven projection, add income/expense, receipt upload, wardrobe cost tracking. |
| Studio | StudioView | Pre-shoot checklist (restaurant confirmed, talent confirmed, consent signed, outfit logged, equipment packed). |
Data access: All tables, all rows. RLS: `role = 'host'` on fd_profiles.
Additional navigation (pushed from tabs, not tab items):
- EpisodeDetailView (from Pipeline)
- ApplicationDetailView (from Inbox)
- ConsentCaptureSheet (from Studio)
- WardrobeManagerView (from Ledger)
- OutfitLogView (from Studio)
- ContentScriptView (from Pipeline/EpisodeDetail)
- MiamiMapView (from Schedule, toolbar button)
Applicant (applied to be on the show)
Tabs: 4 (Series, Apply, Spots, Status)
| Tab | Primary View | Actions |
|---|---|---|
| Series | SeriesView | Watch published episodes with BehindTheGlass overlay. Browse season archive. |
| Apply | ApplicationWizardView | 4-step wizard (About You, Story, Look + Video Selfie, Consent). Edit existing application. |
| Spots | RestaurantsView + MiamiMapView | Browse curated Miami restaurants, neighborhood filter, map pins. |
| Status | ApplicationStatusView | Track application through pipeline. See selection notification. |
Data access: Own profile, own application, published episodes, all restaurants, own matches/messages.
Role transition: User becomes `applicant` the moment they submit an application (server-side trigger on fd_applications INSERT sets profile.role = 'applicant' if currently 'viewer').
Viewer (watching, not applying)
Tabs: 4 (Series, Discover, Spots, Matches)
| Tab | Primary View | Actions |
|---|---|---|
| Series | SeriesView | Watch published episodes. Waitlist position visible. |
| Discover | DiscoverView | Personality quiz swipe (not match-seeking). Results feed icebreaker suggestions. |
| Spots | RestaurantsView + MiamiMapView | Browse restaurants, neighborhood filter. |
| Matches | MatchListView | View quiz-based matches, chat, propose date. |
Data access: Own profile, published episodes, active restaurants, own swipes/matches/messages/dates.
App Root Routing
// FirstDateApp.swift
var body: some Scene {
WindowGroup {
Group {
if authManager.isLoading {
SplashView()
} else if !authManager.isAuthenticated {
AuthView()
} else if let profile = profileManager.profile {
switch profile.role {
case .host:
HostTabView()
case .applicant:
if !profile.onboardingComplete {
OnboardingView()
} else {
ApplicantTabView()
}
case .viewer:
if !profile.onboardingComplete {
OnboardingView()
} else {
ViewerTabView()
}
}
} else {
OnboardingView()
}
}
}
}---
3. Flywheel Architecture
Flywheel 1: Growth-Revenue
Waitlist (fd_waitlist)
↓ referral_code generates position bump
↓ waitlist_count auto-injects into pitch templates
Pitch (fd_episodes.pitch_stage)
↓ comp_meal → logo_placement → title_sponsor progression
↓ deal_value populates fd_ledger_entries on confirmation
Sponsor Content (fd_episodes published + sponsor attribution)
↓ view_count / like_count drive CPV in FinancialsView
↓ sponsor ROI report auto-generated per episode
Distribution (social sharing cards via ImageRenderer)
↓ share card includes waitlist count as social proof
↓ referral link embedded in share URL
Referral (fd_waitlist.referred_by)
↓ 5-spot bump triggers push notification
↓ cycle restarts: larger waitlist → stronger pitchesTables involved: fd_waitlist, fd_episodes, fd_ledger_entries, fd_restaurants
Services involved: EpisodeManager (deal methods), LedgerManager, WaitlistManager
Views involved: FinancialsView, EpisodePipelineView (deals section), WaitlistPositionBadge
Flywheel 2: Content-Experience
Date (fd_dates completed)
↓ post-date review captures vibe tags, would_meet_again
Review (fd_date_reviews)
↓ review data seeds content script Hook/Build/Payoff template
Script (fd_content_scripts)
↓ script drives edit, published episode references review moments
Content (fd_episodes published)
↓ published episode metadata feeds BehindTheGlass overlay
↓ episode reactions (view_count, comment_count) inform icebreaker context
Icebreaker (fd_icebreakers)
↓ Gemini Flash generates conversation starters from shared episode reactions
↓ icebreakers improve match conversations → more dates
Better Date (fd_dates proposed)
↓ cycle restarts: richer conversation history → more interesting dates → better contentTables involved: fd_dates, fd_date_reviews, fd_content_scripts, fd_episodes, fd_icebreakers
Services involved: DateManager, ContentScriptManager, EpisodeManager, IcebreakerManager
Views involved: ChatView (icebreaker injection), SeriesView (BehindTheGlass), ContentScriptView
Flywheel Accelerators (features that speed BOTH loops)
1. BehindTheGlass overlay on published episodes: Content loop gets richer episodes. Growth loop gets sponsor visibility (deal tier shown as "Presented by [Restaurant]").
2. Social sharing cards: Content loop spreads episodes. Growth loop drives waitlist sign-ups via referral links embedded in cards.
3. Application video selfies: Content loop gets casting teaser clips. Growth loop demonstrates audience engagement to sponsors.
---
4. Content Production Pipeline
Episode Lifecycle (10 stages)
PITCH → OUTREACH → NEGOTIATION → CONFIRMED → CASTING → SCHEDULED → FILMED → EDITING → REVIEW → PUBLISHEDMapped to `pitch_stage` enum (first 4) and `status` enum (last 6):
-- pitch_stage tracks the deal pipeline BEFORE production begins
CREATE TYPE fd_pitch_stage AS ENUM (
'identified', -- restaurant identified as target
'outreach', -- initial contact made
'negotiation', -- terms being discussed
'confirmed' -- deal signed, ready for production
);
-- status tracks the production pipeline AFTER deal is confirmed
-- (existing enum, unchanged)
-- 'planning', 'casting', 'scheduled', 'filmed', 'editing', 'published'An episode starts with `pitch_stage = 'identified'` and `status = 'planning'`. Once `pitch_stage` reaches `'confirmed'`, status advances to `'casting'`. The two enums are sequential, not parallel.
Consent Ritual Design (Transparency as Format)
Consent is not a checkbox. It is a face-to-face signing ceremony designed to be filmed.
ConsentCaptureSheet flow:
1. Host opens ConsentCaptureSheet from StudioView checklist
2. Full consent text displayed (scrollable, not collapsed)
3. Participant reads consent text aloud (optional, but encouraged for content)
4. Participant types their full legal name (typed_name field, not just a signature)
5. Participant draws signature on SignatureCanvasView (Canvas + DragGesture)
6. System captures: consent_text_version (SHA-256 hash of displayed text), typed_name, signature image, device_info, timestamp
7. Upload signature to fd-consent-signatures bucket
8. Insert immutable consent record (INSERT only, no UPDATE policy)
9. Episode checklist item turns green
Consent text versioning: The consent text lives in a `fd_consent_text_versions` table. Each version has a hash. When the consent text changes (legal update), a new version is inserted. Consent records reference the specific version hash, so we can always prove which text was agreed to.
struct ConsentCaptureSheet: View {
let episode: Episode
let participant: Profile
@State private var typedName: String = ""
@State private var signatureImage: UIImage?
@State private var consentTextVersion: ConsentTextVersion
// ... SignatureCanvasView, full text display, submit flow
}BehindTheGlass Mechanism
Each published episode carries one curated production detail, chosen by the host at publish time.
ALTER TABLE fd_episodes ADD COLUMN behind_the_glass_type TEXT
CHECK (behind_the_glass_type IN ('deal_story', 'casting_decision', 'outfit_cost', 'restaurant_secret', 'consent_moment'));
ALTER TABLE fd_episodes ADD COLUMN behind_the_glass_text TEXT;In SeriesView, published episodes show a translucent overlay card (glassmorphism, 0.15 opacity background) with the BehindTheGlass detail. Viewers see the economics ("This episode was a comp meal deal worth $0"), the casting logic ("Picked Maya from 47 applicants because..."), or the production secret ("The restaurant's best table is actually table 6 in the back corner").
---
5. Data Architecture
Tables: FINAL (17 tables)
Existing Tables (10, with modifications noted)
| Table | Modifications | Purpose |
|---|---|---|
| `fd_profiles` | +`role fd_role`, +`location geography(Point,4326)`, +`referral_code TEXT UNIQUE`, -`is_admin` | User profiles. Role replaces admin boolean. PostGIS point for distance. |
| `fd_restaurants` | +`location geography(Point,4326)` (populated from existing lat/lon) | Curated Miami restaurants. 25 seeded. |
| `fd_swipes` | No changes | Swipe records for personality quiz (viewer) or match (applicant) |
| `fd_matches` | No changes | Mutual matches |
| `fd_messages` | No changes | Real-time chat |
| `fd_dates` | No changes | Date proposals |
| `fd_date_reviews` | No changes | Post-date reviews with vibe tags |
| `fd_episodes` | +`season_number INT DEFAULT 1`, +`pitch_stage fd_pitch_stage`, +`behind_the_glass_type TEXT`, +`behind_the_glass_text TEXT`, +`hook_timestamp INTERVAL`, +`conflict_timestamp INTERVAL`, +`resolution_timestamp INTERVAL` | Episode records. pitch_stage replaces killed fd_pitches table. |
| `fd_applications` | +`video_selfie_url TEXT`, +`personality_answers JSONB` | Casting applications. Video selfie captured in-app. |
| `fd_consent_records` | +`typed_name TEXT NOT NULL`, +`consent_text_version TEXT NOT NULL` (SHA-256 hash reference) | Immutable. INSERT only. Florida two-party compliance. |
Existing Tables (5, unchanged)
| Table | Purpose |
|---|---|
| `fd_ledger_entries` | Income/expense ledger per episode |
| `fd_wardrobe_items` | Wardrobe tracking with sponsor attribution |
| `fd_outfits` | Episode outfit assignments |
| `fd_waitlist` | Email waitlist for pre-launch |
| `fd_content` | Draft/published content pieces |
New Tables (4)
-- 1. Consent text versions (legal compliance)
CREATE TABLE fd_consent_text_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
version_hash TEXT UNIQUE NOT NULL, -- SHA-256 of consent_text
consent_text TEXT NOT NULL, -- full legal text
effective_from TIMESTAMPTZ DEFAULT now(),
superseded_at TIMESTAMPTZ, -- NULL = current version
created_at TIMESTAMPTZ DEFAULT now()
);
-- 2. Content scripts (Hook/Build/Payoff templates)
CREATE TABLE fd_content_scripts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
episode_id UUID REFERENCES fd_episodes(id) ON DELETE CASCADE,
platform TEXT NOT NULL CHECK (platform IN ('tiktok', 'instagram', 'youtube')),
hook_text TEXT,
build_text TEXT,
payoff_text TEXT,
cta_text TEXT,
generated_by TEXT DEFAULT 'manual' CHECK (generated_by IN ('manual', 'ai')),
is_final BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 3. Icebreakers cache (one-shot per match)
CREATE TABLE fd_icebreakers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
match_id UUID REFERENCES fd_matches(id) ON DELETE CASCADE UNIQUE,
icebreakers JSONB NOT NULL, -- array of 3 strings
generated_at TIMESTAMPTZ DEFAULT now(),
model TEXT DEFAULT 'gemini-2.0-flash'
);
-- 4. Rate limits (swipes, super likes)
CREATE TABLE fd_rate_limits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID REFERENCES fd_profiles(id) ON DELETE CASCADE,
action TEXT NOT NULL CHECK (action IN ('swipe', 'super_like')),
window_start TIMESTAMPTZ NOT NULL,
count INTEGER DEFAULT 0,
UNIQUE(profile_id, action, window_start)
);Device tokens (new, for push notifications)
CREATE TABLE fd_device_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID REFERENCES fd_profiles(id) ON DELETE CASCADE,
token TEXT NOT NULL,
platform TEXT DEFAULT 'ios',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(profile_id, token)
);Total: 20 tables (15 existing + 5 new)
KILLED Tables (and why)
| Killed | Reason |
|---|---|
| `fd_pitches` | Redundant. pitch_stage enum on fd_episodes tracks the same pipeline without a separate table. One episode = one deal = one row. |
| `fd_pitch_activities` | Redundant. Activity tracking on a killed table. Deal notes field on fd_episodes is sufficient for a 1-person operation. |
| `fd_content_metrics` | Redundant. view_count, like_count, comment_count already exist on fd_episodes. Manual entry works initially; platform API fetch is cycle 2. |
| `fd_push_tokens` | Naming duplicate of fd_device_tokens. One table, one name. |
| `fd_analytics` | Deferred. Buffered analytics can use Supabase's built-in pg_stat or a simple events JSONB column. Not worth a dedicated table at launch. |
PostGIS Setup (Phase 0)
CREATE EXTENSION IF NOT EXISTS postgis;
-- Add geography columns
ALTER TABLE fd_profiles ADD COLUMN location geography(Point, 4326);
ALTER TABLE fd_restaurants ADD COLUMN geolocation geography(Point, 4326);
-- Backfill restaurants from existing lat/lon
UPDATE fd_restaurants
SET geolocation = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
-- Discovery feed with distance
CREATE OR REPLACE FUNCTION fd_get_discovery_feed(
p_user_id UUID,
p_max_distance_km FLOAT DEFAULT 50,
p_limit INT DEFAULT 20
) RETURNS TABLE(...) AS $$
-- existing logic + ST_DWithin filter
-- fallback: if user.location IS NULL, use neighborhood text match
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Index for spatial queries
CREATE INDEX idx_fd_profiles_location ON fd_profiles USING gist(location);
CREATE INDEX idx_fd_restaurants_geolocation ON fd_restaurants USING gist(geolocation);RLS Additions
All new tables get RLS enabled at creation. Key policies:
-- fd_consent_records: INSERT only for authenticated, no UPDATE, no DELETE
CREATE POLICY "Insert consent" ON fd_consent_records
FOR INSERT TO authenticated
WITH CHECK (true);
-- No UPDATE or DELETE policies. Consent records are immutable.
-- fd_consent_text_versions: read-only for all authenticated
CREATE POLICY "Read consent text" ON fd_consent_text_versions
FOR SELECT TO authenticated USING (true);
-- Only host can insert new versions (managed via migration, not app)
-- fd_content_scripts: host only
CREATE POLICY "Host manages scripts" ON fd_content_scripts
FOR ALL TO authenticated
USING (EXISTS (SELECT 1 FROM fd_profiles WHERE user_id = auth.uid() AND role = 'host'));
-- fd_icebreakers: participants of the match only
CREATE POLICY "Match participants read icebreakers" ON fd_icebreakers
FOR SELECT TO authenticated
USING (match_id IN (
SELECT id FROM fd_matches
WHERE profile_a IN (SELECT id FROM fd_profiles WHERE user_id = auth.uid())
OR profile_b IN (SELECT id FROM fd_profiles WHERE user_id = auth.uid())
));
-- fd_device_tokens: own tokens only
CREATE POLICY "Own tokens" ON fd_device_tokens
FOR ALL TO authenticated
USING (profile_id IN (SELECT id FROM fd_profiles WHERE user_id = auth.uid()))
WITH CHECK (profile_id IN (SELECT id FROM fd_profiles WHERE user_id = auth.uid()));Existing RLS Fixes (from review)
6 HIGH-severity gaps to fix in Phase 0:
1. fd_profiles: Users can UPDATE any profile. Fix: add `USING (user_id = auth.uid())` to UPDATE policy.
2. fd_swipes: No RLS. Fix: enable RLS, own-data policies.
3. fd_matches: No RLS. Fix: enable RLS, participants-only SELECT.
4. fd_messages: No RLS. Fix: enable RLS, match-participants only.
5. fd_dates: No RLS. Fix: enable RLS, match-participants only.
6. fd_date_reviews: No RLS. Fix: enable RLS, own-data for INSERT, match-participants for SELECT.
---
6. Service Architecture
FINAL Service List (16 services)
All services follow the manager-level DI pattern:
@MainActor
final class ExampleManager: ObservableObject {
static let shared = ExampleManager()
private let client: SupabaseClient
// Production init (singleton)
private init() {
self.client = SupabaseConfig.client
}
// Test init (injected client)
init(client: SupabaseClient) {
self.client = client
}
}| Service | Status | Responsibilities |
|---|---|---|
| AuthManager | EXISTS | Auth flow, deep links, session management |
| ProfileManager | EXISTS, MODIFY | Load/update profile. Add: location permission request, PostGIS point update, role-based access helpers |
| DiscoveryManager | EXISTS, MODIFY | Swipe feed. Add: PostGIS distance filter with neighborhood centroid fallback, deal-breaker filters (age range, cuisine), super-like (1/day server-enforced) |
| MatchManager | EXISTS | Match list, mutual match check |
| ChatManager | EXISTS, MODIFY | Realtime messages. Add: icebreaker injection on empty conversation |
| DateManager | EXISTS | Propose, confirm, complete dates |
| RestaurantManager | EXISTS, MODIFY | Restaurant list. Add: MapKit annotations, distance sorting |
| ApplicationManager | EXISTS, MODIFY | Submit/track applications. Add: video selfie upload, 4-step wizard state, personality quiz answers |
| EpisodeManager | EXISTS, MODIFY | Episode CRUD. ABSORBS pitch pipeline (updatePitchStage, updateDeal). Add: season_number, behind_the_glass, consent gating (server-side: reject status='scheduled' without consent_record_id), content link timestamps |
| LedgerManager | EXISTS, MODIFY | P&L tracking. Add: receipt photo upload, breakeven projection calculation, category auto-suggestion |
| ConsentManager | EXISTS, MODIFY | Consent records. Add: signature canvas capture, consent text version fetching, typed name validation |
| DashboardManager | EXISTS, MODIFY | Stats aggregation. Add: revenue projection, per-episode ROI, sponsor CPV |
| PhotoManager | EXISTS, MODIFY | Photo upload. Add: signed URL generation, crop/resize, reorder, minimum-1 enforcement |
| WardrobeManager | NEW | Wardrobe CRUD, outfit logging per episode, sponsor attribution, cost tracking |
| ContentScriptManager | NEW | Generate/edit Hook-Build-Payoff-CTA scripts per platform per episode |
| NotificationManager | NEW | APNS registration, token management, deep link routing, 4 notification categories (match, message, application_update, episode_published) |
| WaitlistManager | NEW | Waitlist signup, referral code generation, position tracking, 5-spot bump logic |
MERGED Services (from review)
| Original | Merged Into | Reason |
|---|---|---|
| PitchManager | EpisodeManager | pitch_stage is a field on fd_episodes, not a separate entity. `updatePitchStage()` and `updateDeal()` are methods on EpisodeManager. |
Key Method Signatures
// EpisodeManager -- new methods (absorbed from killed PitchManager)
func updatePitchStage(_ episodeId: String, stage: FDPitchStage) async throws
func advanceToProduction(_ episodeId: String) async throws // pitch_stage=confirmed → status=casting
func setBehindTheGlass(_ episodeId: String, type: String, text: String) async throws
func loadSeasonEpisodes(season: Int) async
// ConsentManager -- new methods
func captureConsent(episodeId: String, participantId: String, typedName: String, signatureImage: UIImage) async throws
func getCurrentConsentText() async throws -> ConsentTextVersion
// NotificationManager
func registerForPush() async throws
func handleNotification(_ userInfo: [AnyHashable: Any]) -> DeepLink?
enum NotificationCategory: String { case match, message, applicationUpdate, episodePublished }
// WaitlistManager
func joinWaitlist(email: String, referralCode: String?) async throws -> WaitlistEntry
func getPosition(email: String) async throws -> Int
func generateReferralCode() -> String
// ContentScriptManager
func generateScript(episodeId: String, platform: ScriptPlatform) async throws -> ContentScript
func updateScript(_ scriptId: String, section: ScriptSection, text: String) async throws
enum ScriptPlatform: String, Codable { case tiktok, instagram, youtube }
enum ScriptSection { case hook, build, payoff, cta }---
7. View Architecture
Host Navigation Tree
HostTabView
├── Tab 0: Inbox
│ └── ApplicationInboxView (filtered pipeline: pending → reviewing → shortlisted → selected → passed)
│ └── push → ApplicationDetailView
│ ├── sheet → AssignToEpisodeSheet
│ └── sheet → VideoPreviewSheet
│
├── Tab 1: Pipeline
│ └── EpisodePipelineView (Kanban + season selector + 10-week progress bar)
│ ├── section → DealsSection (pitch_stage pipeline inline, NOT separate tab)
│ └── push → EpisodeDetailView
│ ├── sheet → ContentScriptView (per-platform script editor)
│ ├── sheet → BehindTheGlassEditor
│ ├── push → ClipTimelineView (hook/conflict/resolution timestamps)
│ └── push → SponsorReportView (per-episode ROI, CPV)
│
├── Tab 2: Schedule
│ └── ProductionCalendarView (month view, episode blocks, conflict warnings)
│ ├── sheet → ScheduleEpisodeSheet (date picker, time slot, restaurant)
│ └── toolbar → MiamiMapView (episode-colored pins, restaurant detail sheet)
│
├── Tab 3: Ledger
│ └── FinancialsView (P&L summary, category breakdown, breakeven projection)
│ ├── section → RevenueSection (income by category, deal pipeline value)
│ ├── section → ExpenseSection (expenses by category, receipt gallery)
│ ├── sheet → AddLedgerEntrySheet (type, category, amount, receipt photo, episode link)
│ ├── push → WardrobeManagerView (grid, add item, sponsor attribution)
│ └── push → BreakevenProjectionView (chart, assumptions editor)
│
└── Tab 4: Studio
└── StudioView (pre-shoot checklist per episode)
├── Checklist: Restaurant confirmed ✓/✗
├── Checklist: Talent confirmed ✓/✗
├── Checklist: Consent signed ✓/✗ → sheet → ConsentCaptureSheet
├── Checklist: Outfit logged ✓/✗ → push → OutfitLogView
└── Checklist: Equipment packed ✓/✗Applicant Navigation Tree
ApplicantTabView
├── Tab 0: Series
│ └── SeriesView (published episodes, season selector)
│ └── push → EpisodePlayerView
│ └── overlay → BehindTheGlassOverlay (translucent production detail)
│
├── Tab 1: Apply
│ └── ApplicationWizardView (4-step)
│ ├── Step 1: AboutYouStep (name, age, bio, neighborhood)
│ ├── Step 2: StoryStep (why_me, personality quiz)
│ ├── Step 3: LookStep (photos + video selfie capture)
│ └── Step 4: ConsentStep (acknowledge terms, submit)
│ └── [post-submit] → ApplicationConfirmationView
│
├── Tab 2: Spots
│ └── RestaurantsView (cards, neighborhood filter)
│ ├── push → RestaurantDetailView
│ └── toolbar → MiamiMapView
│
└── Tab 3: Status
└── ApplicationStatusView (current stage, timeline, what's next)
└── [if selected] → push → PreProductionView (consent signing, scheduling)Viewer Navigation Tree
ViewerTabView
├── Tab 0: Series
│ └── SeriesView (published episodes, season selector)
│ └── push → EpisodePlayerView
│ └── overlay → BehindTheGlassOverlay
│
├── Tab 1: Discover
│ └── DiscoverView (swipe cards, personality quiz framing)
│ ├── overlay → MatchPopupView (on mutual right-swipe)
│ └── swipe-up → SuperLikeAnimation (1/day)
│
├── Tab 2: Spots
│ └── RestaurantsView (cards, neighborhood filter)
│ ├── push → RestaurantDetailView
│ └── toolbar → MiamiMapView
│
└── Tab 3: Matches
└── MatchListView (matches with last message preview)
└── push → ChatView
├── Icebreaker injection (if conversation empty, show 3 AI-generated starters)
└── push → ProposeDateView → DateConfirmationViewThe Membrane: What Production Data Leaks Through
The "membrane" is the boundary between host-only production data and what applicants/viewers can see.
| Production Data | Visible To | How It Leaks |
|---|---|---|
| Episode status (planning → editing) | Host only | Nothing leaks until published |
| Episode (published) | Everyone | SeriesView |
| BehindTheGlass detail | Everyone | Curated by host at publish time. ONE detail per episode. |
| Deal tier | Viewers via BehindTheGlass | Only if host chooses 'deal_story' type |
| Outfit cost | Viewers via BehindTheGlass | Only if host chooses 'outfit_cost' type |
| Casting decision rationale | Viewers via BehindTheGlass | Only if host chooses 'casting_decision' type |
| Consent ritual | Everyone | Filmed as content. The signing is a scene. |
| Waitlist count | Sponsor pitch decks | Auto-injected number, not visible in app |
| Restaurant details | Everyone | RestaurantsView (curated subset: name, cuisine, vibe, photo, neighborhood) |
| Restaurant filming notes | Host only | Never leaks |
| Application video selfies | Host only | Could become teaser content if applicant consents (future) |
| P&L, ledger, expenses | Host only | Never leaks (except via BehindTheGlass if host chooses) |
---
8. Edge Functions
1. `fd-generate-icebreakers`
Trigger: Database webhook on `fd_matches` INSERT (new mutual match created).
Logic:
1. Fetch both profiles (interests, preferred_cuisines, shared episode reactions)
2. Call Gemini 2.0 Flash with context prompt:
Generate 3 conversation starters for two people who matched on FirstDate,
a Miami dating show. Profile A: {interests, cuisines}. Profile B: {interests, cuisines}.
Shared context: {any episodes both reacted to}.
Make them specific, playful, and reference Miami.3. Insert result into fd_icebreakers (one-shot, never regenerated)
4. Fallback: If Gemini fails or times out (5s), insert 3 generic icebreakers from a pre-written pool of 30, selected by matching cuisine preference.
Runtime: Deno, Supabase JS client.
2. `fd-push-notification`
Trigger: Database webhook on fd_messages INSERT, fd_matches INSERT, fd_applications UPDATE (status change), fd_episodes UPDATE (status = 'published').
Logic:
1. Determine notification category from trigger table
2. Fetch recipient's device token from fd_device_tokens
3. Build APNS payload with deep link URL
4. Send via HTTP/2 to `api.push.apple.com` using .p8 JWT auth
5. Fallback: If APNS returns 410 (unregistered), mark token `is_active = false`. If network error, log to `fd_push_failures` (simple JSONB append on the token row). No retry queue at launch.
APNS JWT: .p8 key stored as Supabase secret (`APNS_KEY_P8`, `APNS_KEY_ID`, `APNS_TEAM_ID`). JWT generated per-request (short-lived).
3. `fd-consent-gating`
Trigger: Database trigger (BEFORE UPDATE) on fd_episodes when status is being set to 'scheduled'.
Logic:
1. Check if `consent_record_id` is NOT NULL on the episode
2. If NULL, raise exception: `'Cannot schedule episode without signed consent'`
3. Verify consent_record_id exists in fd_consent_records
4. Fallback: This is a hard gate. No fallback. Episode cannot advance without consent. This is a legal requirement, not a nice-to-have.
Implementation: PostgreSQL trigger function (not a Deno edge function). Runs synchronously inside the transaction.
CREATE OR REPLACE FUNCTION fd_enforce_consent_gating()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status = 'scheduled' AND NEW.consent_record_id IS NULL THEN
RAISE EXCEPTION 'Cannot schedule episode without signed consent record';
END IF;
IF NEW.status = 'scheduled' AND NOT EXISTS (
SELECT 1 FROM fd_consent_records WHERE id = NEW.consent_record_id
) THEN
RAISE EXCEPTION 'Consent record does not exist';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_episode_consent_gating
BEFORE UPDATE ON fd_episodes
FOR EACH ROW
WHEN (NEW.status = 'scheduled' AND OLD.status != 'scheduled')
EXECUTE FUNCTION fd_enforce_consent_gating();---
9. Design Principles
1. The Episode Is the Product
Every feature must answer: "Does this make the next episode better, easier to produce, or more likely to be watched?" If the answer is no, it does not ship in this cycle. The dating mechanics exist to feed the content pipeline. The restaurant browser exists to source locations. The waitlist exists to prove audience to sponsors.
2. Transparency Is the Format, Not the Liability
Production details (consent rituals, deal economics, casting decisions, outfit costs) are not hidden behind admin screens. They are content. BehindTheGlass is not a debugging view; it is a feature that audiences watch. Consent signing is not a legal formality; it is a scene. The show's unique angle is that the audience sees how the sausage is made.
3. Three Roles, Not Two
The system is not admin/user. It is host/applicant/viewer. Each role has distinct motivations: the host produces, the applicant hopes, the viewer watches. The navigation, data access, and experience are different for each. Never collapse these into a binary. The role enum on fd_profiles is the source of truth, and RLS policies branch on it.
4. Seasons Are Boundaries
Every episode belongs to a season. A season is a bounded 10-week arc. Between seasons, the app enters archive mode: published episodes are browsable, the waitlist reopens for the next season, and the dining guide persists as evergreen content. Seasons prevent scope creep ("just one more episode") and create natural marketing moments ("Season 2 casting is open").
5. One Table, One Truth
No parallel tracking. An episode's deal lives on the episode row (pitch_stage), not in a separate pitches table. Metrics live on the episode row, not in a separate metrics table. If a field belongs to exactly one entity, it is a column on that entity's table, not a foreign key to a shadow table. This principle killed 4 tables in review.
---
10. Anti-Patterns
DO NOT: Create Parallel Tracking Tables
The review killed `fd_pitches`, `fd_pitch_activities`, and `fd_content_metrics` because they duplicated data that belongs on `fd_episodes`. If you find yourself creating a table where every row has a 1:1 foreign key back to fd_episodes, stop. Add columns to fd_episodes instead.
DO NOT: Wrap SupabaseClient at the Protocol Level
supabase-swift v2's concrete types (`PostgrestFilterBuilder`, `RealtimeChannel`, etc.) are not protocol-friendly without massive wrapper boilerplate. Inject at the MANAGER level: each manager takes `init(client: SupabaseClient)` that defaults to the shared singleton. Tests create a real SupabaseClient pointing at a test project or mock the manager itself. Do not attempt `SupabaseClientProtocol`.
DO NOT: Add a 6th Tab to HostTabView
Apple HIG recommends 5 or fewer tabs. The review caught this: Deals was proposed as a 6th tab. Deals is a section inside Pipeline, not a separate destination. If a new feature needs a tab, something else must become a pushed view or a sheet.
DO NOT: Make Consent Records Mutable
`fd_consent_records` has INSERT-only RLS. No UPDATE policy. No DELETE policy. If a consent needs to be "revoked," insert a new record with `consent_type = 'revocation'`. The original signed record remains in the database forever. This is a legal requirement for Florida two-party consent.
DO NOT: Ship Push Notifications Without the .p8 Key Path
The APNS integration requires a .p8 key from Apple Developer portal, stored as a Supabase secret. Before writing any push notification code, verify the key exists. If it does not exist, register it via ASC. The edge function is useless without it. Check this in Phase 0.
DO NOT: Use `print(error)` in New Code
All error handling goes through the `FDError` enum with the `.fdErrorAlert()` view modifier. The existing codebase has `print(error)` in every manager. New code must not add more. Existing `print(error)` calls get replaced as each manager is touched.
DO NOT: Defer Phases as "Future Work"
The evolution compound specified 7 phases. The review compressed to 6 phases over 15 days. All 6 phases execute. Content scripts are not "future work." Wardrobe management is not "nice to have." The breakeven projection is not "if we have time." If it is in this architecture document, it ships.
DO NOT: Launch Without RLS on Every Table
Every table gets RLS enabled and at least one policy at creation time. The Phase 0 migration fixes 6 existing gaps. No new table is created without simultaneous policy creation in the same migration file. A table without RLS is a security incident waiting to happen.
---
Appendix: Migration File Plan
| Migration | Contents |
|---|---|
| `001_security_foundation.sql` | PostGIS extension, fd_role enum, role column + backfill, drop is_admin, location columns on profiles + restaurants, backfill restaurant geolocation, 6 RLS fixes, fd_rate_limits table + RLS, fd_consent_text_versions table + RLS, camera/mic entitlement notes (reminder only, entitlements are Xcode-side) |
| `002_production_revenue.sql` | fd_pitch_stage enum, pitch_stage + season_number + behind_the_glass columns on fd_episodes, fd_content_scripts table + RLS, consent_record additions (typed_name, consent_text_version), consent immutability policies, consent gating trigger |
| `003_growth_intelligence.sql` | fd_icebreakers table + RLS, fd_device_tokens table + RLS, referral_code on fd_profiles, video_selfie_url + personality_answers on fd_applications, spatial indexes, updated discovery feed RPC with PostGIS + deal-breaker filters |
---
Appendix: Existing → Architecture Diff Summary
| Category | Before | After |
|---|---|---|
| Tables | 15 | 20 (+5 new, -0 existing) |
| Killed table proposals | 0 | 4 (fd_pitches, fd_pitch_activities, fd_content_metrics, fd_push_tokens) |
| Services | 13 | 16 (+4 new, -0 existing, +1 absorbed merge) |
| Views (Swift files) | 35 | ~65 (+30 new views) |
| Roles | 2 (admin/user) | 3 (host/applicant/viewer) |
| Tab configurations | 2 (HostTabView 5 tabs, PublicTabView 4 tabs) | 3 (HostTabView 5 tabs, ApplicantTabView 4 tabs, ViewerTabView 4 tabs) |
| Edge functions | 0 | 2 Deno + 1 PL/pgSQL trigger |
| RLS policies | 31 | ~48 |
| Seasons concept | None | season_number on episodes, archive mode between seasons |
| Distance filtering | Neighborhood text only | PostGIS + neighborhood centroid fallback |
| Consent model | Signature image only | Signature + typed name + text version hash + immutable records |
Promotion Decision
Promote into a technical note or architecture paper with implementation anchors.
Source Anchor
omega-output/firstdate-mega-build-20260320/04-architecture.md
Detected Structure
Method · Evaluation · References · Math · Code Anchors · Architecture · is Stage Research