SDD Technical Brief

Swipe File Dashboard

v1.0.0 · Caio (HW Network) · 2026-04-05 · Pre-production

01 — Problem Statement

The operational gap

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

Three user archetypes

User 1: Pedro (Creative Ops) — Primary daily operator

User 2: Caio (Creative Strategist) — Query & oversight

User 3: Future creative ops (scalability)

03 — Technology Stack

The stack

LayerTechnologyRationale
DatabaseSupabase (PostgreSQL)Already connected as MCP, Claude Code can query directly, Row Level Security, real-time subscriptions
Backend/APISupabase (auto-generated REST + RPC)Zero backend code needed — Supabase PostgREST handles CRUD, RPC handles complex queries
FrontendNext.js 14+ (App Router)React-based, SSR for fast loads, API routes if needed, Vercel-native deploy
AuthSupabase Auth (email/password)Simple, integrated, supports RLS per-user
DeployVercelAlready connected as MCP, auto-deploy from GitHub, preview URLs per branch
Query interfaceClaude Code + Supabase MCPCaio queries the database in natural language, Claude translates to SQL
Local referenceObsidian (read-only sync)Optional — a Claude Code agent can export filtered subsets as markdown to Obsidian vault

Key architectural decisions

Why NOT a low-code tool (Retool, Appsmith, NocoDB)

Why NOT Obsidian Dataview as the primary interface

Why Supabase over raw PostgreSQL

04 — Data Model

Schema definition

Table: creatives

The 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();

Table: offers

Lookup 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()
);

Table: briefings

Links 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
);

Row Level Security

-- 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)

The interface spec

5.1 Creative Entry Form (Create/Edit)

A multi-section form with:

Section 1: Identity

Section 2: ISA Classification

Section 3: Content Classification

Section 4: Quality & Usability

Section 5: Content

Section 6: Performance (conditional — only if is_own_creative is checked)

Behavior: Auto-save draft after 5s of inactivity (debounced). Validation: title is required, at least hook_type or copy_angle must be filled. On save: immediate feedback (toast notification). Edit mode: same form, pre-populated with existing data.

5.2 Creative List View (Read)

The main working view. A filterable, searchable table/grid of all creatives.

Display columns (default)

Filters (sidebar or top bar)

Search

Full-text search across title, copy_angle, first_15s_transcription, notes, key_phrases. Uses the fts tsvector column for fast search.

Sort

Pagination

25 items per page, cursor-based (Supabase range queries).

5.3 Creative Detail View

5.4 Bulk Operations

5.5 Offers Management

Simple CRUD for the offers lookup table: Add/edit/archive offers. Each offer shows count of associated creatives.

5.6 Briefings View (Future Phase)

06 — Claude Code Integration

Natural language queries

Query patterns (via Supabase MCP)

"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'

Obsidian sync (optional, future)

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

Next.js project layout

creatives-dashboard/ ├── app/ │ ├── layout.tsx # Root layout (auth check, theme provider) │ ├── page.tsx # Dashboard home (creative list view) │ ├── login/ │ │ └── page.tsx # Auth page │ ├── creative/ │ │ ├── new/ │ │ │ └── page.tsx # Create creative form │ │ └── [id]/ │ │ └── page.tsx # Creative detail/edit view │ ├── offers/ │ │ └── page.tsx # Offers CRUD │ └── api/ │ └── export/ │ └── route.ts # CSV export endpoint ├── components/ │ ├── creative-form.tsx # Multi-section form component │ ├── creative-list.tsx # Filterable list/table │ ├── creative-card.tsx # Card component for grid view │ ├── creative-detail.tsx # Detail panel │ ├── filter-sidebar.tsx # Filter controls │ ├── search-bar.tsx # Full-text search input │ ├── tag-input.tsx # Multi-tag input component │ ├── quality-badge.tsx # Quality indicator │ ├── classification-select.tsx # Styled dropdown for taxonomy fields │ ├── bulk-actions.tsx # Bulk operation controls │ ├── toast.tsx # Notification component │ └── auth/ │ ├── login-form.tsx │ └── auth-provider.tsx # Supabase auth context ├── lib/ │ ├── supabase/ │ │ ├── client.ts # Browser Supabase client │ │ ├── server.ts # Server Supabase client │ │ └── types.ts # Auto-generated TypeScript types │ ├── constants/ │ │ └── taxonomy.ts # ISA taxonomy labels, colors, mappings │ └── utils/ │ ├── filters.ts # Filter builder utilities │ ├── search.ts # Search query helpers │ └── export.ts # CSV export logic ├── public/ ├── .env.local ├── package.json ├── tailwind.config.ts ├── tsconfig.json └── next.config.js

08 — Implementation Phases

Five-day roadmap

Phase 1 — Foundation (Day 1)

Phase 2 — Core CRUD (Days 2–3)

Phase 3 — Filters & Search (Day 4)

Phase 4 — Polish & Operations (Day 5)

Phase 5 — UI/UX Application (Day 6)

09 — Environment Variables

Configuration

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

Scope boundaries

Constraints

Non-goals (v1)

11 — Open Questions

Decisions pending

  1. Supabase project: Use existing project or create a new one dedicated to creatives.?
  2. Supabase Storage: Use it for thumbnails/video files, or just store URLs?
  3. Domain: Custom domain (e.g. creatives.hwnetwork.com) or Vercel default URL?
  4. Auth method: Email/password only, or add magic link?

12 — Handoff Notes

Ready for execution

This SDD is ready for Claude Code execution. The implementation sequence:

  1. Feed this document to Claude Code as context
  2. Provide the UI/UX briefing (color scheme, fonts, component aesthetic) as a second input
  3. Claude Code runs the Supabase migration, scaffolds Next.js, implements CRUD, deploys to Vercel
  4. Iterate on visual polish with the UI/UX briefing
The data model is the most critical piece — get it right first, UI can iterate.