Swipe File Dashboard
01 — Problem Statement
Caio is a creative strategist at HW Network who writes direct-response ad scripts (hooks, microleads, VSL bodies) for the American 50+ supplement market. His workflow depends on a swipe file — a categorized database of competitor ads, reference creatives, and his own past work — classified using a proprietary taxonomy (ISA + Creative DNA).
Current pain: Notion databases cannot handle this data via API reliably. The Supabase Table Editor is too raw for daily operational use. Pedro (Creative Ops) needs a simple, browser-based interface to input, browse, filter, and manage 500–2000 creative entries daily. Caio needs the same data queryable via Claude Code (natural language) and Obsidian (local markdown reference).
Core insight: The database backend (Supabase) and the working interface (dashboard) are separate concerns. Supabase stores and serves. The dashboard is where humans work. Claude Code is where Caio queries.
02 — Users & Access Patterns
03 — Technology Stack
| Layer | Technology | Rationale |
|---|---|---|
| Database | Supabase (PostgreSQL) | Already connected as MCP, Claude Code can query directly, Row Level Security, real-time subscriptions |
| Backend/API | Supabase (auto-generated REST + RPC) | Zero backend code needed — Supabase PostgREST handles CRUD, RPC handles complex queries |
| Frontend | Next.js 14+ (App Router) | React-based, SSR for fast loads, API routes if needed, Vercel-native deploy |
| Auth | Supabase Auth (email/password) | Simple, integrated, supports RLS per-user |
| Deploy | Vercel | Already connected as MCP, auto-deploy from GitHub, preview URLs per branch |
| Query interface | Claude Code + Supabase MCP | Caio queries the database in natural language, Claude translates to SQL |
| Local reference | Obsidian (read-only sync) | Optional — a Claude Code agent can export filtered subsets as markdown to Obsidian vault |
04 — Data Model
creativesThe core table. Each row = one ad creative (competitor or own).
CREATE TABLE creatives (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
created_by UUID REFERENCES auth.users(id),
-- Identity
title TEXT NOT NULL,
source_url TEXT,
source_platform TEXT CHECK (source_platform IN ('meta', 'youtube', 'tiktok', 'other')),
local_file_path TEXT,
thumbnail_url TEXT,
-- Classification: ISA Taxonomy
hook_type TEXT CHECK (hook_type IN (
'curiosity', 'fear', 'social_proof', 'conditional', 'imperative',
'defeat', 'result', 'walkthrough', 'open_loop', 'question',
'contrast', 'authority_claim'
)),
hook_technique TEXT CHECK (hook_technique IN (
'tratamento_errado', 'cena_universal', 'mecanismo_direto',
'burden_emocional', 'discovery_first', 'friendly_fire',
'resultado_prova', 'confissao'
)),
opening_device TEXT CHECK (opening_device IN (
'question', 'statement', 'conditional', 'command',
'story', 'statistic', 'contrast'
)),
-- Classification: Content
mechanism TEXT CHECK (mechanism IN (
'ingredient', 'process', 'discovery', 'deficiency',
'combination', 'lifestyle_change'
)),
tone TEXT CHECK (tone IN (
'urgent', 'empathetic', 'authority', 'conversational',
'alarmist', 'confessional', 'scientific', 'conspiratorial'
)),
cta_type TEXT CHECK (cta_type IN (
'quiz', 'free_report', 'doctor_consult', 'video',
'direct_purchase', 'limited_offer'
)),
visual_format TEXT CHECK (visual_format IN (
'green_screen', 'podcast_clip', 'car_confession',
'walking_talking', 'screen_recording', 'ugc_testimonial',
'broll_vo', 'slideshow'
)),
-- Classification: Quality
quality_tag TEXT CHECK (quality_tag IN ('strong', 'medium', 'weak')),
usability TEXT CHECK (usability IN (
'hook_only', 'full_structure', 'angle_inspiration',
'voc_source', 'visual_reference'
)),
-- Content
copy_angle TEXT,
first_15s_transcription TEXT,
key_phrases TEXT[],
full_transcription TEXT,
notes TEXT,
-- Metadata
duration_seconds INTEGER,
niche TEXT CHECK (niche IN (
'weight_loss', 'joint_pain', 'brain_health',
'gut_health', 'blood_sugar', 'heart_health',
'general_wellness', 'other'
)),
offer TEXT,
language TEXT DEFAULT 'en',
-- Performance (for own creatives)
is_own_creative BOOLEAN DEFAULT false,
ctr DECIMAL(5,2),
cpm DECIMAL(8,2),
hook_rate DECIMAL(5,2),
vturb_retention_15s DECIMAL(5,2),
vturb_retention_30s DECIMAL(5,2),
vturb_retention_60s DECIMAL(5,2),
spend DECIMAL(10,2),
status TEXT CHECK (status IN (
'draft', 'in_production', 'live', 'analyzing', 'iterate', 'dead'
)),
-- Tags (flexible)
tags TEXT[]
);
-- Indexes for common query patterns
CREATE INDEX idx_creatives_hook_type ON creatives(hook_type);
CREATE INDEX idx_creatives_niche ON creatives(niche);
CREATE INDEX idx_creatives_offer ON creatives(offer);
CREATE INDEX idx_creatives_quality ON creatives(quality_tag);
CREATE INDEX idx_creatives_is_own ON creatives(is_own_creative);
CREATE INDEX idx_creatives_status ON creatives(status);
CREATE INDEX idx_creatives_created ON creatives(created_at DESC);
CREATE INDEX idx_creatives_tags ON creatives USING GIN(tags);
CREATE INDEX idx_creatives_key_phrases ON creatives USING GIN(key_phrases);
-- Full text search on transcriptions and copy angle
ALTER TABLE creatives ADD COLUMN fts tsvector
GENERATED ALWAYS AS (
to_tsvector('english',
coalesce(title, '') || ' ' ||
coalesce(copy_angle, '') || ' ' ||
coalesce(first_15s_transcription, '') || ' ' ||
coalesce(notes, '')
)
) STORED;
CREATE INDEX idx_creatives_fts ON creatives USING GIN(fts);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER creatives_updated_at
BEFORE UPDATE ON creatives
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
offersLookup table for active offers.
CREATE TABLE offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
niche TEXT NOT NULL,
target_demographic TEXT,
funnel_type TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
);
briefingsLinks creatives to writing sessions.
CREATE TABLE briefings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT now(),
created_by UUID REFERENCES auth.users(id),
offer_id UUID REFERENCES offers(id),
title TEXT NOT NULL,
piece_type TEXT CHECK (piece_type IN ('hook', 'microlead', 'vsl_body', 'full_vsl')),
direction_notes TEXT,
status TEXT CHECK (status IN ('pending', 'in_writing', 'review', 'done')) DEFAULT 'pending',
reference_creative_ids UUID[],
voc_entries JSONB,
performance_context JSONB
);
-- Enable RLS
ALTER TABLE creatives ENABLE ROW LEVEL SECURITY;
ALTER TABLE offers ENABLE ROW LEVEL SECURITY;
ALTER TABLE briefings ENABLE ROW LEVEL SECURITY;
-- All authenticated users can read all data
CREATE POLICY "Authenticated users can read creatives"
ON creatives FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert creatives"
ON creatives FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "Authenticated users can update creatives"
ON creatives FOR UPDATE TO authenticated USING (true);
-- Same for offers and briefings
CREATE POLICY "read_offers" ON offers FOR SELECT TO authenticated USING (true);
CREATE POLICY "insert_offers" ON offers FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_offers" ON offers FOR UPDATE TO authenticated USING (true);
CREATE POLICY "read_briefings" ON briefings FOR SELECT TO authenticated USING (true);
CREATE POLICY "insert_briefings" ON briefings FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_briefings" ON briefings FOR UPDATE TO authenticated USING (true);
05 — Dashboard Features (CRUD)
A multi-section form with:
offers table)is_own_creative is checked)The main working view. A filterable, searchable table/grid of all creatives.
Full-text search across title, copy_angle, first_15s_transcription, notes, key_phrases. Uses the fts tsvector column for fast search.
25 items per page, cursor-based (Supabase range queries).
Simple CRUD for the offers lookup table: Add/edit/archive offers. Each offer shows count of associated creatives.
06 — Claude Code Integration
"Show me all strong curiosity hooks for weight loss"
SELECT * FROM creatives
WHERE quality_tag = 'strong'
AND hook_type = 'curiosity'
AND niche = 'weight_loss'
"What are our top 5 performing hooks by CTR?"
SELECT title, ctr, hook_type, offer
FROM creatives
WHERE is_own_creative = true AND ctr IS NOT NULL
ORDER BY ctr DESC LIMIT 5
"Find ads that mention GLP-1 in the transcription"
SELECT * FROM creatives
WHERE fts @@ to_tsquery('english', 'GLP-1')
"Compare hook rate between curiosity and fear hooks on BurnSlim"
SELECT hook_type, AVG(hook_rate), COUNT(*)
FROM creatives
WHERE is_own_creative = true
AND offer = 'BurnSlim'
AND hook_type IN ('curiosity', 'fear')
GROUP BY hook_type
"What hooks have we not tried on CoreStrength?"
SELECT DISTINCT hook_type FROM creatives
WHERE niche = 'joint_pain' AND quality_tag = 'strong'
EXCEPT
SELECT DISTINCT hook_type FROM creatives
WHERE is_own_creative = true AND offer = 'CoreStrength'
A Claude Code agent or cron job can export filtered views as markdown. Each exported note = one creative with YAML frontmatter (for Dataview) + body content.
07 — File Structure
08 — Implementation Phases
09 — Environment Variables
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ... # Server-side only, never exposed to client
10 — Constraints & Non-Goals
11 — Open Questions
creatives.?12 — Handoff Notes
This SDD is ready for Claude Code execution. The implementation sequence: