Stage 4: FORGE — FirstDate Architecture
**Core principle:** The deal IS the episode. A restaurant sponsorship deal produces one episode. An episode fulfills one deal. They share a lifecycle.
Full Public Reader
Stage 4: FORGE — FirstDate Architecture
The Formula: Content Business OS
One app. Two experiences. One host, many applicants, episodic content, real money tracking.
Core principle: The deal IS the episode. A restaurant sponsorship deal produces one episode. An episode fulfills one deal. They share a lifecycle.
Role-Based Navigation
// FirstDateApp.swift — role switching via isAdmin
if profile?.isAdmin == true {
HostTabView() // Mohamed's experience
} else {
PublicTabView() // Women's / audience experience
}### Host Tabs (Mohamed)
1. Inbox — Application review (approve/pass/shortlist)
2. Pipeline — Deal CRM (Lead → Pitched → Confirmed → Filmed → Published → Invoiced)
3. Schedule — 10-week calendar grid (deal + talent + budget per week)
4. Ledger — P&L with expense/revenue categorization
5. Studio — Consent manager + pre-shoot checklist + episode content links
### Public Tabs (Women / Audience)
1. Series — Published episodes (poster cards, content links, restaurant featured)
2. Apply — Application form (photo, bio, Instagram, why-me, consent acknowledgment)
3. Spots — Restaurant catalog with "Featured in Episode X" badges
4. Status — Application status tracker (pending → reviewing → selected/not selected)
5. Chat — DMs with Mohamed (unlocked after selection)
Data Models (New + Modified)
New Models
// Models/Application.swift
struct Application: Codable, Identifiable {
let id: String
let profileId: String // FK → fd_profiles
var status: ApplicationStatus // pending, reviewing, shortlisted, selected, passed
var whyMe: String // Free text: "Why should Mohamed take you on a date?"
var instagramHandle: String // Required
var videoIntroUrl: String? // Optional hosted video link
var consentAcknowledged: Bool // Acknowledged filming consent info
let episodeId: String? // Linked when selected for an episode
var reviewedAt: String?
let createdAt: String
}
enum ApplicationStatus: String, Codable {
case pending, reviewing, shortlisted, selected, passed
}
// Models/Episode.swift — unified with Deal
struct Episode: Codable, Identifiable {
let id: String
var weekNumber: Int // 1-10
var title: String // "Week 3: Coconut Grove"
var status: EpisodeStatus // planning, casting, scheduled, filmed, editing, published
// Deal fields (restaurant sponsorship)
var restaurantId: String // FK → fd_restaurants
var dealTier: DealTier? // comp_meal, logo_placement, title_sponsor
var dealValue: Double? // Cash value of sponsorship
var dealContactName: String?
var dealContactEmail: String?
var dealNotes: String?
var pitchedAt: String?
var confirmedAt: String?
var invoicedAt: String?
var paidAt: String?
// Talent fields
var applicationId: String? // FK → fd_applications (selected woman)
var talentName: String?
var talentInstagram: String?
// Production fields
var scheduledFor: String?
var timeSlot: String? // brunch, lunch, dinner
var filmedAt: String?
var consentRecordId: String? // FK → fd_consent_records
// Content fields
var tiktokUrl: String?
var instagramUrl: String?
var youtubeUrl: String?
var thumbnailUrl: String?
var publishedAt: String?
// Metrics (updated manually or via content platform APIs later)
var viewCount: Int?
var likeCount: Int?
var commentCount: Int?
let createdAt: String
var updatedAt: String
}
enum EpisodeStatus: String, Codable, CaseIterable {
case planning, casting, scheduled, filmed, editing, published
}
enum DealTier: String, Codable {
case compMeal = "comp_meal"
case logoPlacement = "logo_placement"
case titleSponsor = "title_sponsor"
}
// Models/ConsentRecord.swift
struct ConsentRecord: Codable, Identifiable {
let id: String
let episodeId: String
let participantProfileId: String
var consentType: String // "filming_and_distribution"
var signatureImageUrl: String // Supabase Storage URL
var signedAt: String
var deviceInfo: String // iPhone model + iOS version
var ipAddress: String?
let createdAt: String
}
// Models/LedgerEntry.swift
struct LedgerEntry: Codable, Identifiable {
let id: String
var type: LedgerType // income or expense
var category: LedgerCategory
var amount: Double
var description: String
var episodeId: String? // Which episode this relates to
var receiptUrl: String? // Photo of receipt (Supabase Storage)
var date: String
var taxDeductible: Bool
let createdAt: String
}
enum LedgerType: String, Codable {
case income, expense
}
enum LedgerCategory: String, Codable, CaseIterable {
// Income
case sponsorshipCash = "sponsorship_cash"
case sponsorshipComp = "sponsorship_comp"
case affiliateRevenue = "affiliate_revenue"
case contentMonetization = "content_monetization"
case brandDeal = "brand_deal"
// Expense
case food, transport, wardrobe, production, appInfra = "app_infra", misc
}
// Models/WardrobeItem.swift
struct WardrobeItem: Codable, Identifiable {
let id: String
var name: String // "Navy Slim Chinos"
var brand: String // "Zara"
var category: String // top, bottom, shoes, accessories, fragrance
var cost: Double
var isSponsored: Bool
var sponsorName: String?
var photoUrl: String?
var episodesWorn: [String] // Episode IDs
let createdAt: String
}
// Models/Outfit.swift
struct Outfit: Codable, Identifiable {
let id: String
var episodeId: String
var wardrobeItemIds: [String]
var photoUrl: String? // Full outfit photo
var notes: String?
let createdAt: String
}Modified Models
// Restaurant — add sponsorship fields
struct Restaurant: Codable, Identifiable {
// ... existing fields ...
var sponsorshipTier: DealTier? // NEW
var featuredInEpisode: Int? // NEW — episode week number
var partnerContactName: String? // NEW
var partnerContactEmail: String? // NEW
var filmingPermission: Bool? // NEW
var lightingNotes: String? // NEW
var bestTableDescription: String? // NEW
}New Supabase Tables (7)
-- fd_applications
CREATE TABLE fd_applications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID REFERENCES fd_profiles(id),
status TEXT DEFAULT 'pending',
why_me TEXT NOT NULL,
instagram_handle TEXT NOT NULL,
video_intro_url TEXT,
consent_acknowledged BOOLEAN DEFAULT false,
episode_id UUID,
reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- fd_episodes (unified deal + episode)
CREATE TABLE fd_episodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
week_number INTEGER NOT NULL CHECK (week_number BETWEEN 1 AND 20),
title TEXT,
status TEXT DEFAULT 'planning',
restaurant_id UUID REFERENCES fd_restaurants(id),
deal_tier TEXT,
deal_value NUMERIC(10,2),
deal_contact_name TEXT,
deal_contact_email TEXT,
deal_notes TEXT,
pitched_at TIMESTAMPTZ,
confirmed_at TIMESTAMPTZ,
invoiced_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
application_id UUID REFERENCES fd_applications(id),
talent_name TEXT,
talent_instagram TEXT,
scheduled_for TIMESTAMPTZ,
time_slot TEXT,
filmed_at TIMESTAMPTZ,
consent_record_id UUID,
tiktok_url TEXT,
instagram_url TEXT,
youtube_url TEXT,
thumbnail_url TEXT,
published_at TIMESTAMPTZ,
view_count INTEGER DEFAULT 0,
like_count INTEGER DEFAULT 0,
comment_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- fd_consent_records
CREATE TABLE fd_consent_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
episode_id UUID REFERENCES fd_episodes(id),
participant_profile_id UUID REFERENCES fd_profiles(id),
consent_type TEXT DEFAULT 'filming_and_distribution',
signature_image_url TEXT NOT NULL,
signed_at TIMESTAMPTZ NOT NULL,
device_info TEXT,
ip_address TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- fd_ledger_entries
CREATE TABLE fd_ledger_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL CHECK (type IN ('income', 'expense')),
category TEXT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
description TEXT,
episode_id UUID REFERENCES fd_episodes(id),
receipt_url TEXT,
date DATE NOT NULL,
tax_deductible BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
-- fd_wardrobe_items
CREATE TABLE fd_wardrobe_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
brand TEXT,
category TEXT,
cost NUMERIC(10,2) DEFAULT 0,
is_sponsored BOOLEAN DEFAULT false,
sponsor_name TEXT,
photo_url TEXT,
episodes_worn UUID[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
-- fd_outfits
CREATE TABLE fd_outfits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
episode_id UUID REFERENCES fd_episodes(id),
wardrobe_item_ids UUID[] NOT NULL,
photo_url TEXT,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Add columns to fd_restaurants
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS sponsorship_tier TEXT;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS featured_in_episode INTEGER;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS partner_contact_name TEXT;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS partner_contact_email TEXT;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS filming_permission BOOLEAN;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS lighting_notes TEXT;
ALTER TABLE fd_restaurants ADD COLUMN IF NOT EXISTS best_table_description TEXT;New Services (7)
| Service | Responsibility |
|---|---|
| ApplicationManager | Fetch/filter/approve/reject/shortlist applications |
| EpisodeManager | Full episode lifecycle: plan → cast → schedule → film → edit → publish. Also handles deal pipeline stages. |
| ConsentManager | Generate consent form, capture signature (PencilKit canvas), upload to Storage, create record |
| LedgerManager | Income/expense CRUD, per-episode P&L, series P&L, category breakdown |
| WardrobeManager | Wardrobe item CRUD, outfit assembly per episode |
| ScheduleManager | 10-week plan computation from episodes, budget allocation per week |
| ChecklistManager | Pre-shoot validation: consent signed? Reservation confirmed? Phone charged? Outfit logged? |
New Views (~25)
### Host Views (Mohamed-only)
- `Views/Host/HostTabView.swift` — 5-tab layout
- `Views/Host/Inbox/ApplicationInboxView.swift` — card list with approve/pass/shortlist
- `Views/Host/Inbox/ApplicationDetailView.swift` — full applicant profile
- `Views/Host/Pipeline/EpisodePipelineView.swift` — horizontal Kanban (planning → casting → ... → published)
- `Views/Host/Pipeline/EpisodeDetailView.swift` — all episode fields + deal info + talent + content links
- `Views/Host/Schedule/ScheduleView.swift` — 10-week grid
- `Views/Host/Schedule/WeekDetailView.swift` — expanded week: budget, logistics, deal status
- `Views/Host/Ledger/LedgerView.swift` — transaction list with category filters
- `Views/Host/Ledger/PLSummaryView.swift` — series P&L with visual breakdown
- `Views/Host/Ledger/AddExpenseView.swift` — quick expense entry with receipt photo
- `Views/Host/Studio/StudioView.swift` — consent records + pre-shoot checklist
- `Views/Host/Studio/ConsentCaptureView.swift` — legal text + PencilKit signature + timestamp
- `Views/Host/Studio/SignatureCanvasView.swift` — PencilKit drawing canvas
- `Views/Host/Studio/PreShootChecklistView.swift` — gated validation before filming
- `Views/Host/Studio/WardrobeView.swift` — wardrobe items + outfit assembly
- `Views/Host/Studio/OutfitBuilderView.swift` — select items for an episode outfit
### Public Views (Women / Audience)
- `Views/Public/PublicTabView.swift` — 5-tab layout
- `Views/Public/Series/SeriesView.swift` — episode poster cards
- `Views/Public/Series/PublicEpisodeDetailView.swift` — episode info + content links + restaurant card
- `Views/Public/Apply/ApplicationFormView.swift` — replaces OnboardingView for applicants
- `Views/Public/Apply/ApplicationStatusView.swift` — pending/reviewing/selected/passed
- `Views/Public/Spots/PublicSpotsView.swift` — restaurants with "Featured in Episode X" badges
- `Views/Public/Status/StatusView.swift` — application tracker
File Inventory (Post-Rebuild)
### Preserved (12 files)
- FirstDateApp.swift (modified for role switching)
- AuthManager, AuthView (unchanged)
- SupabaseConfig (unchanged)
- Theme.swift (add custom fonts)
- Constants.swift (add new constants)
- ChatManager, ChatView, MessageBubbleView (kept for Mohamed-applicant DMs)
- PhotoManager (finally wired to UI)
- GradientButton, LoadingView, ProfilePhotoView, ChipSelector, SingleChipSelector
### Deleted (12 files)
- DiscoverView, SwipeCardView, MatchPopupView, DiscoveryManager
- MatchListView, MatchRowView, MatchManager
- DatesView, DateCardView, ProposeDateView, DateManager
- DashboardView (replaced by role-specific dashboards)
### New (~32 files)
- 7 models
- 7 services
- 16 host views
- 7 public views
- StatCardView kept (reused in Ledger)
Net: ~42 files, ~6,200 LOC (from 41 files, ~4,500 LOC)
Design Evolution
### Typography Upgrade
Add custom fonts for the Calvin Klein-esque aesthetic:
- Headlines: DM Serif Display (Google Fonts) or Playfair Display
- Body: DM Sans (clean, modern, pairs with DM Serif)
- Add font files to Xcode project, register in Info.plist
### Color Palette Refinement
Keep the existing dark theme but add:
static let fdIvory = Color(hex: "f5f0e8") // Warm text on dark (replaces pure white)
static let fdChampagne = Color(hex: "e8d5b7") // Luxury accent
static let fdOnyx = Color(hex: "0d0d14") // Deepest dark### Motion
- Episode cards: parallax scroll effect
- Pipeline: horizontal drag with snap-to-stage
- Consent signature: ink trail animation
- P&L numbers: counting animation on appear
## Anti-Patterns to Avoid
1. No WKWebView embeds — TikTok embeds break. Just use URL links that open in Safari/app.
2. No camera integration — iPhone Camera app is better. Use PhotosPicker for receipt photos.
3. No audience voting — Dehumanizes applicants. Killed in Review.
4. No leaderboards of women — Ranking by follower count is creepy.
5. No "talent" or "deal" language in public UI — Internal only. Public says "application" and "episode."
Promotion Decision
Promote into a technical note or architecture paper with implementation anchors.
Source Anchor
omega-output/firstdate-rebuild-20260320/04-architecture.md
Detected Structure
Method · References · Code Anchors · Architecture · is Stage Research