Dominad

October 19, 2025 · View on GitHub

AI-powered competitive intelligence platform for Google Ads advertisers

Built with Next.js, Mastra, CopilotKit, and Supabase

Next.js TypeScript Supabase Mastra License

FeaturesArchitectureGetting StartedAPI DocsDevelopment


📑 Table of Contents


Overview

Dominad is a comprehensive competitive intelligence platform that helps marketers track competitors, scrape Google Ads campaigns, analyze advertising strategies with AI, and generate actionable insights. The platform combines web scraping (Apify), multimodal AI analysis (OpenAI GPT, Google Gemini), and an intelligent agent system (Mastra) to deliver deep competitor insights.

What Dominad Does

  1. Scrapes competitor Google Ads via Apify (YouTube video ads, text ads, image ads)
  2. Analyzes ad creatives with AI using GPT-4o for text/images and Gemini for videos
  3. Tracks ad campaigns with normalized database schema for variations and regional stats
  4. Generates strategic reports identifying patterns, themes, and opportunities
  5. Provides AI canvas for strategy planning and campaign ideation with CopilotKit
  6. Manages competitors with full CRUD operations and user-scoped data isolation

✨ Key Features

Core Capabilities

  • 🔍 Competitor Tracking Dashboard: Monitor competitors with Google Advertiser IDs, track ad activity, and manage watchlists
  • 📊 AI-Powered Ad Analysis: Analyze video (Gemini), image (GPT-4o Vision), and text ads with structured insights
  • 📈 Reports Management: Generate bulk analysis reports with filtering, pattern detection, and strategic recommendations
  • 🎨 AI Canvas: Interactive strategy planning with CopilotKit, Google Ads preview generator, and real-time AI synchronization
  • 🌐 Google Ads Scraping: Apify integration for scraping YouTube video ads, text ads, and image ads from Google Transparency Center
  • 📄 Landing Page Analysis: Firecrawl-powered extraction and AI analysis of ad landing pages for cohesion scoring
  • 🔐 Authentication: Supabase Auth with email/password, protected routes, and user-scoped data isolation

Ad Analysis Features

  • Video Analysis: Download YouTube videos, analyze with Gemini's multimodal capabilities, extract hooks/CTAs/messaging
  • Image Analysis: Vision-based analysis of display ads with OpenAI GPT-4o, text extraction, brand detection
  • Text Analysis: Copywriting analysis with headline/description/CTA evaluation and keyword extraction
  • Bulk Analysis: Batch processing of multiple ads with pattern detection and aggregated insights
  • Component Analysis: Deep dive into specific creative aspects (hooks, CTAs, social proof, problem-solution frameworks)
  • Landing Page Cohesion: Compare ad messaging with landing page content for consistency scoring

Data Management

  • Normalized Database Schema: Separate tables for ad creatives, variations, and regional stats for efficient querying
  • Historical Tracking: Time-series data for impression tracking and ad lifecycle monitoring
  • Ad Deduplication: Intelligent deduplication based on creative IDs and advertiser IDs
  • Repository Pattern: All database access through type-safe repositories with Drizzle ORM
  • User Scoped Data: All competitors, ads, and reports isolated by userId for multi-tenant security

🏗️ System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                           Dominad Platform                             │
│                   (Next.js 15 App Router + TypeScript)                  │
└─────────────────────────────────────────────────────────────────────────┘

                ┌───────────────────┼───────────────────┐
                │                   │                   │
         ┌──────▼──────┐     ┌──────▼──────┐    ┌──────▼───────┐
         │  Frontend   │     │   Backend   │    │  Data Layer  │
         │  (React)    │     │  (API       │    │  (Supabase   │
         │             │     │   Routes)   │    │  PostgreSQL) │
         └──────┬──────┘     └──────┬──────┘    └──────┬───────┘
                │                   │                   │
    ┌───────────┼───────────────────┼───────────────────┼──────────┐
    │           │                   │                   │          │
    ▼           ▼                   ▼                   ▼          ▼
┌───────┐  ┌────────┐        ┌──────────┐      ┌──────────┐  ┌──────┐
│ AI    │  │Copilot │        │  Mastra  │      │ Drizzle  │  │Supa- │
│Canvas │  │  Kit   │        │  Agents  │      │   ORM    │  │base  │
│       │  │        │        │          │      │          │  │Auth  │
└───┬───┘  └────────┘        └────┬─────┘      └──────────┘  └──┬───┘
    │                             │                             │
    └─────────────────────────────┼─────────────────────────────┘

                    ┌─────────────┼─────────────┐
                    │             │             │
              ┌─────▼────┐  ┌─────▼────┐  ┌────▼─────┐
              │  Apify   │  │ OpenAI   │  │  Gemini  │
              │ (Scrape) │  │  GPT-4o  │  │ (Video)  │
              └──────────┘  └──────────┘  └──────────┘
                    │             │             │
              ┌─────▼─────────────▼─────────────▼────┐
              │    External Services & APIs           │
              │  Firecrawl • YouTube • DataForSEO    │
              └───────────────────────────────────────┘

Data Flow Pipeline

┌──────────────────────────────────────────────────────────────────────┐
│                      Ad Scraping → Analysis Flow                     │
└──────────────────────────────────────────────────────────────────────┘

1. SCRAPING
   User → Competitor Detail Page → "Scrape Ads" Button

   /api/scrape-google-ads → Apify Google Ads Scraper

   Raw Apify Data (creatives, variations, region stats)

   apify-transformer.ts (Normalization)

   Database (ad_creatives, ad_variations, ad_region_stats)

2. ANALYSIS (Triggered by user or auto-fetch)
   User → Ad Card → "Analyze" Button

   ┌──────────────────┼──────────────────┐
   │                  │                  │
   Video Ad       Image Ad          Text Ad
   │                  │                  │
   ↓                  ↓                  ↓
/api/analyze-      /api/analyze-    /api/analyze-
video-ad           image-ad         text-ad
   │                  │                  │
   ↓                  ↓                  ↓
Gemini API         GPT-4o Vision    GPT-4o
(Multimodal)       (Image)          (Text)
   │                  │                  │
   └──────────────────┼──────────────────┘

              Store in ad_insights
              (aiAnalysis/imageAnalysis/textAnalysis)

3. REPORTING (Bulk analysis with Mastra agents)
   User → Reports Page → "Generate Report"

   Select filters (date range, platform, format)

   /api/analyze-image-text-ads

   Mastra imageTextReportAgent

   Batch analysis with tools (imageAdAnalysis, textAdAnalysis)

   Aggregated insights + patterns

   Store in reports table (with reportAds junction)

Application Structure

dominad/
├── src/
│   ├── app/                          # Next.js App Router
│   │   ├── (auth)/                   # Auth route group (login/signup)
│   │   ├── dashboard/                # Protected dashboard routes
│   │   │   ├── competitors/          # Competitor listing + detail
│   │   │   ├── reports/              # Reports listing + detail
│   │   │   └── layout.tsx            # Dashboard layout with sidebar
│   │   ├── api/                      # API route handlers
│   │   │   ├── competitors/          # CRUD for competitors
│   │   │   ├── ads/                  # CRUD for ads
│   │   │   ├── reports/              # CRUD for reports
│   │   │   ├── scrape-google-ads/    # Apify scraping
│   │   │   ├── analyze-video-ad/     # Gemini video analysis
│   │   │   ├── analyze-image-ad/     # GPT-4o image analysis
│   │   │   ├── analyze-text-ad/      # GPT-4o text analysis
│   │   │   ├── analyze-image-text-ads/ # Batch analysis with Mastra
│   │   │   ├── copilotkit/           # CopilotKit runtime
│   │   │   └── proxy-image/          # CORS proxy for images
│   │   └── page.tsx                  # AI Canvas (home page)
│   ├── components/                   # React components
│   │   ├── dashboard/                # Dashboard-specific components
│   │   │   ├── CompetitorsTable.tsx
│   │   │   ├── ReportsTable.tsx
│   │   │   ├── AdTimeline.tsx
│   │   │   └── CopilotSidebar.tsx
│   │   ├── canvas/                   # AI canvas components
│   │   └── ui/                       # shadcn/ui components
│   ├── lib/                          # Shared libraries
│   │   ├── db/                       # Database layer
│   │   │   ├── schema.ts             # Drizzle schema definitions
│   │   │   ├── index.ts              # Database client
│   │   │   └── repositories/         # Data access layer
│   │   ├── supabase/                 # Supabase integration
│   │   │   ├── client.ts             # Client-side auth
│   │   │   ├── server.ts             # Server-side auth
│   │   │   └── middleware.ts         # Session management
│   │   ├── dashboard/                # Dashboard utilities
│   │   │   ├── apify-transformer.ts  # Apify data normalization
│   │   │   └── workflow.ts           # Workflow definitions
│   │   └── logger.ts                 # Pino logging with correlation IDs
│   └── mastra/                       # Mastra AI configuration
│       ├── index.ts                  # Mastra instance + agent registry
│       ├── agents/                   # Agent definitions
│       │   ├── index.ts              # Canvas agent
│       │   ├── analysis-agent.ts     # Analysis agent
│       │   └── image-text-report-agent.ts
│       └── tools/                    # Mastra tools
│           ├── index.ts              # Planning tools
│           ├── firecrawl-scrape-with-schema.ts
│           ├── find-competitors.ts
│           ├── image-ad-analysis.ts
│           └── text-ad-analysis.ts
├── middleware.ts                     # Next.js middleware (auth)
├── drizzle.config.ts                 # Drizzle ORM config
├── package.json                      # Dependencies
└── .env.example                      # Environment variables template

🗄️ Database Architecture

Dominad uses Supabase (PostgreSQL) with Drizzle ORM for type-safe database access. The schema is normalized to support efficient querying and historical tracking.

Database Schema

┌─────────────────────────────────────────────────────────────────────┐
│                          DATABASE SCHEMA                            │
└─────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│  auth.users      │  (Supabase Auth - managed)
│──────────────────│
│ id (PK)          │
│ email            │
│ created_at       │
└────────┬─────────┘

         │ 1:N


┌──────────────────┐         ┌──────────────────┐
│  competitors     │         │ analysis_tasks   │
│──────────────────│         │──────────────────│
│ id (PK)          │ ←───────│ competitor_id    │
│ userId (FK) ─────┼─────┐   │ userId (FK)      │
│ name             │     │   │ type             │
│ googleAdvertiser │     │   │ status           │
│ website          │     │   │ parameters       │
│ domain           │     │   │ progress         │
│ status           │     │   │ results          │
│ targetCountries  │     │   └──────────────────┘
│ targetLanguages  │     │
│ niche            │     │
│ industry         │     │
└────────┬─────────┘     │
         │               │
         │ 1:N           │
         │               │
         ↓               │
┌──────────────────┐     │   ┌──────────────────┐
│  ad_creatives    │     │   │  reports         │
│──────────────────│     │   │──────────────────│
│ id (PK)          │     │   │ id (PK)          │
│ competitor_id────┼─────┘   │ userId (FK) ─────┼────┐
│ advertiser_id    │ ←───────│ competitor_id    │    │
│ format           │         │ type             │    │
│ platform         │         │ analysisType     │    │
│ videoUrl         │         │ filters          │    │
│ previewUrl       │         │ results          │    │
│ firstSeenDate    │         │ insights         │    │
│ lastSeenDate     │         └─────────┬────────┘    │
│ status           │                   │             │
│ impressions      │                   │ 1:N         │
│ views            │                   │             │
│ regions          │                   ↓             │
└────────┬─────────┘         ┌──────────────────┐    │
         │                   │  report_ads      │    │
         │ 1:N               │──────────────────│    │
         │                   │ id (PK)          │    │
         ↓                   │ report_id (FK)   │    │
┌──────────────────┐         │ ad_id (FK)       │    │
│ ad_variations    │         └──────────────────┘    │
│──────────────────│                                 │
│ id (PK)          │                                 │
│ creative_id ─────┼─────────────────────────────────┘
│ headline         │
│ description      │
│ clickThroughUrl  │
│ imageUrl         │
│ targetKeywords   │
│ firstSeenDate    │
│ lastSeenDate     │
│ status           │
└──────────────────┘

         │ 1:N


┌──────────────────┐         ┌──────────────────┐
│ ad_region_stats  │         │  ad_insights     │
│──────────────────│         │──────────────────│
│ id (PK)          │         │ id (PK)          │
│ creative_id ─────┼───┐     │ competitor_id───┼─┐
│ regionCode       │   │     │ metricName       │ │
│ regionName       │   │     │ value            │ │
│ firstShown       │   │     │ previousValue    │ │
│ lastShown        │   │     │ trend            │ │
│ impressions      │   │     │ period           │ │
│ snapshotDate     │   │     │ category         │ │
│ surfaceStats     │   │     │ aiAnalysis       │ │
└──────────────────┘   │     │ landingPageAnaly │ │
                       │     │ imageAnalysis    │ │
                       │     │ textAnalysis     │ │
                       │     └──────────────────┘ │
                       │                          │
                       └──────────────────────────┘

Key Tables

competitors

Tracks businesses being monitored for advertising activity.

ColumnTypeDescription
idtextPrimary key (nanoid)
userIdtextForeign key to auth.users (user-scoped data)
nametextCompetitor business name
websitetextFull website URL
domaintextDomain name (e.g., "example.com")
googleAdvertiserIdtextGoogle Advertiser ID for scraping
statustextActive, Inactive, Monitoring
targetCountriesjsonbArray of ISO country codes
targetLanguagesjsonbArray of language codes
nichetextBusiness niche/category
industrytextIndustry classification

Indexes: userId, googleAdvertiserId, status

ad_creatives (NEW - Normalized Schema)

Stores unique ad creatives from Google Ads Transparency Center.

ColumnTypeDescription
idtextApify creative ID (e.g., "CR10435096316668280833")
competitorIdtextForeign key to competitors
advertiserIdtextApify advertiser ID
advertiserNametextAdvertiser name
formattextTEXT, IMAGE, VIDEO
platformtextYouTube, Google Search, Display Network
videoUrltextYouTube URL (for VIDEO ads)
previewUrltextAd preview URL
firstSeenDatetextISO date (earliest across all regions)
lastSeenDatetextISO date (most recent across all regions)
impressionsLowerBoundintegerTotal impressions lower bound
impressionsUpperBoundintegerTotal impressions upper bound
viewsintegerYouTube view count (nullable)
regionsjsonbArray of region codes where ad is shown
statustextActive, Paused, Removed

Indexes: competitorId, platform, status, detectedDate, advertiserId

Note: This replaces the deprecated ads table with a normalized design that separates creatives, variations, and regional stats.

ad_variations (NEW - Normalized Schema)

Stores headline/description combinations for each creative.

ColumnTypeDescription
idtextComposite: ${creative_id}-${hash}
creativeIdtextForeign key to ad_creatives
headlinetextAd headline
descriptiontextAd description
clickThroughUrltextLanding page URL
imageUrltextThumbnail or logo URL
targetKeywordsjsonbArray of keywords
firstSeenDatetextWhen variation first appeared
lastSeenDatetextWhen variation last seen
statustextActive, Paused, Removed

Indexes: creativeId, status, unique index on (creativeId, headline, description)

ad_region_stats (NEW - Normalized Schema)

Time-series data for regional impression tracking.

ColumnTypeDescription
idtextnanoid
creativeIdtextForeign key to ad_creatives
regionCodetextISO country code (FR, US, BE, etc.)
regionNametextFull region name
firstShowntextDate first shown in region (YYYY-MM-DD)
lastShowntextDate last shown in region (YYYY-MM-DD)
impressionsLowerBoundintegerImpressions lower bound
impressionsUpperBoundintegerImpressions upper bound
snapshotDatetextDate of this snapshot (ISO)
surfaceStatsjsonbPlatform-specific stats (Search, YouTube, etc.)

Indexes: creativeId, regionCode, snapshotDate, composite (creativeId, regionCode, snapshotDate)

Purpose: Track impression changes over time for historical analysis.

ad_insights

AI-generated analysis results for ads.

ColumnTypeDescription
idtextPrimary key
competitorIdtextForeign key to competitors
metricNametextMetric identifier
valuerealCurrent metric value
previousValuerealPrevious metric value
trendtextup, down, stable
periodtextTime period (e.g., "last-30-days")
categorytextai-marketing-analysis, frequency, messaging
aiAnalysisjsonbVideo analysis results from Gemini
imageAnalysisjsonbImage analysis results from GPT-4o
textAnalysisjsonbText analysis results from GPT-4o
landingPageAnalysisjsonbLanding page analysis from Firecrawl + GPT-4o

Indexes: competitorId, category, period

reports

AI-generated competitive analysis reports.

ColumnTypeDescription
idtextPrimary key
userIdtextForeign key to auth.users
competitorIdtextForeign key to competitors
typetextBulk Analysis, Component Analysis, Landing Page Analysis
analysisTypetextbulk-ads, component-specific, landing-page-full
filtersjsonbFilter options used (date range, platform, format)
resultsjsonbAnalysis results
insightsjsonbKey insights array
metadatajsonbAdditional metadata
statustextDraft, Published, Archived

Indexes: userId, competitorId, type, analysisType, date

report_ads (Junction Table)

Many-to-many relationship between reports and ads.

ColumnTypeDescription
idtextPrimary key
reportIdtextForeign key to reports
adIdtextForeign key to ad_creatives

Indexes: reportId, adId, unique index on (reportId, adId)

Purpose: Track which specific ads were analyzed in each report.

Migration from Old to New Schema

The platform recently migrated from a denormalized ads table to the new normalized schema:

Old Schema (DEPRECATED):

  • ads table stored everything (creatives, variations, region stats) in JSONB columns
  • Difficult to query variations or regional stats efficiently
  • No historical tracking of impression changes

New Schema (CURRENT):

  • ad_creatives: One row per unique ad creative
  • ad_variations: Multiple rows for different headline/description combinations
  • ad_region_stats: Time-series data for impression tracking by region
  • Efficient querying with proper indexes
  • Historical tracking for trend analysis

Migration Script: See specs/done/refactor-apify-data-flow-normalized-schema.md

Repository Pattern

All database access goes through type-safe repositories in src/lib/db/repositories/:

// Example: Competitors Repository
export const competitorsRepository = {
  async getAll(userId: string): Promise<Competitor[]> { ... },
  async getById(id: string, userId: string): Promise<Competitor | null> { ... },
  async create(data: NewCompetitor): Promise<Competitor> { ... },
  async update(id: string, userId: string, data: Partial<Competitor>): Promise<Competitor> { ... },
  async delete(id: string, userId: string): Promise<void> { ... },
};

Benefits:

  • Type safety with Drizzle ORM inferred types
  • Centralized data access logic
  • Easy to test and mock
  • User-scoped queries built-in for security

🤖 AI Agent System

Dominad uses Mastra, an AI agent framework for TypeScript, to power intelligent analysis workflows. Mastra agents have working memory, tools, and can execute multi-step tasks.

Mastra Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Mastra Framework                        │
└─────────────────────────────────────────────────────────────────┘

                ┌──────────────┼──────────────┐
                │              │              │
        ┌───────▼──────┐  ┌────▼─────┐  ┌────▼──────────┐
        │ Canvas Agent │  │ Analysis │  │ Image/Text    │
        │              │  │  Agent   │  │ Report Agent  │
        └──────┬───────┘  └────┬─────┘  └────┬──────────┘
               │               │               │
               │               │               │
     ┌─────────┴───────┐   ┌───┴────────┐  ┌──┴──────────┐
     │ Tools:          │   │ Memory:    │  │ Tools:      │
     │ - setPlan       │   │ - Analysis │  │ - imageAd   │
     │ - updatePlan    │   │   State    │  │   Analysis  │
     │ - completePlan  │   │ - Postgres │  │ - textAd    │
     │ - find          │   │   Store    │  │   Analysis  │
     │   Competitors   │   └────────────┘  └─────────────┘
     └─────────────────┘

       ┌───────┴────────────┐
       │ Memory:            │
       │ - Agent State      │
       │ - Working Memory   │
       │ - Postgres Store   │
       └────────────────────┘

Agent Definitions

1. Canvas Agent (canvasAgent)

Location: src/mastra/agents/index.ts

Purpose: Powers the AI Canvas interface with CopilotKit for strategy planning and campaign ideation.

Working Memory Schema:

{
  items: Card[],              // Canvas cards (Project, Entity, Note, Chart, Google Ad)
  globalTitle: string,        // Canvas title
  globalDescription: string,  // Canvas description
  planSteps: PlanStep[],      // Multi-step plan progress
  currentStepIndex: number,   // Current step index
  planStatus: string,         // Plan status
  itemsCreated: number,       // Count of items created
}

Tools:

  • setPlan: Initialize a multi-step plan
  • updatePlanProgress: Update plan step status
  • completePlan: Mark plan as completed
  • findCompetitors: Search for competitors using DataForSEO API

Model: OpenAI GPT-4.1

Usage:

  • User interacts with AI canvas via CopilotKit textarea
  • Agent creates/updates cards in real-time with bidirectional state sync
  • Agent can create Google Ads previews with proper formatting
  • Agent tracks multi-step plans with visual progress indicators

Example:

// In canvas page:
const { agentState } = useCoAgent({
  name: "sample_agent",
  initialState: { items: [], globalTitle: "", ... }
});

2. Analysis Agent (analysisAgent)

Location: src/mastra/agents/analysis-agent.ts

Purpose: Dedicated agent for deep ad analysis with multiple analysis modes.

Working Memory Schema:

{
  analysisType: "bulk-ads" | "component-specific" | "landing-page-full",
  competitorName: string,
  competitorId: string,
  adsToAnalyze: Ad[],
  filters: FilterOptions,
  aspect: string,              // For component analysis (hook, CTA, etc.)
  landingPageData: object,     // For landing page analysis
  currentStep: string,
  progress: number,            // 0-100
  analysisResult: object,
  error?: string,
}

Capabilities:

  • Bulk Ad Analysis: Identify patterns, themes, and strategic insights across multiple ads
  • Component Analysis: Deep dive into specific creative aspects (hook, CTA, messaging, social proof)
  • Landing Page Analysis: Evaluate ad-landing page cohesion and identify opportunities

Model: OpenAI GPT-5

Instructions: Expert marketing analyst specializing in competitive ad analysis

Usage:

const agent = mastra.agents.analysis_agent;
const result = await agent.generate({
  messages: [{ role: 'user', content: 'Analyze these ads for patterns' }],
  workingMemory: {
    analysisType: 'bulk-ads',
    competitorName: 'Acme Corp',
    adsToAnalyze: [...ads],
  }
});

3. Image/Text Report Agent (imageTextReportAgent)

Location: src/mastra/agents/image-text-report-agent.ts

Purpose: Batch analysis of image and text ads with cross-format pattern detection.

Working Memory Schema:

{
  imageAds: Ad[],
  textAds: Ad[],
  competitorName: string,
  analysisProgress: number,    // 0-100
  imageResults: AnalysisResult[],
  textResults: AnalysisResult[],
  aggregatedResult: object,
  currentStep: string,
  error?: string,
}

Tools:

  • imageAdAnalysis: Analyze image ads with GPT-4o Vision
  • textAdAnalysis: Analyze text ad copy with GPT-4o

Model: OpenAI GPT-4o

Workflow:

  1. Analyze each image ad individually using vision tool
  2. Analyze each text ad individually using text tool
  3. Aggregate format-specific insights
  4. Synthesize cross-format patterns and recommendations

Usage:

// Called by /api/analyze-image-text-ads
const agent = mastra.agents.image_text_report_agent;
const result = await agent.generate({
  messages: [{ role: 'user', content: 'Analyze these image and text ads' }],
  workingMemory: {
    competitorName: 'Acme Corp',
    imageAds: [...imageAds],
    textAds: [...textAds],
  }
});

Mastra Tools

Location: src/mastra/tools/

Planning Tools

  • setPlan: Initialize plan steps
  • updatePlanProgress: Update step status (pending, in_progress, completed, blocked, failed)
  • completePlan: Mark plan as done

Analysis Tools

  • imageAdAnalysis: Extract visual elements, text, branding from images
  • textAdAnalysis: Analyze headlines, descriptions, CTAs, messaging
  • firecrawlScrapeWithSchema: Scrape landing pages with structured schema
  • findCompetitors: Search DataForSEO for advertisers running Google Ads

Working Memory & State Management

Mastra agents use working memory backed by PostgreSQL (via @mastra/pg):

// Example: Canvas agent memory
memory: new Memory({
  storage: new PostgresStore({ connectionString: process.env.DATABASE_URL! }),
  options: {
    workingMemory: {
      enabled: true,
      schema: AgentState,  // Zod schema
    },
  },
})

Benefits:

  • State persists across requests
  • Agents can access previous context
  • CopilotKit syncs frontend state with agent memory
  • Type-safe with Zod schemas

Integration with CopilotKit

┌─────────────────────────────────────────────────────────────────┐
│                  CopilotKit Integration Flow                    │
└─────────────────────────────────────────────────────────────────┘

Frontend (React)             Backend (API Route)           Mastra Agent
─────────────────            ───────────────────           ────────────

useCoAgent hook    ───────>  /api/copilotkit  ────────>   canvasAgent
      │                           │                             │
      │                           │                             │
  agentState         <───────     │                             │
      │                           │                             │
  (React state)      ───────>  Runtime         ────────>    Memory
      │                        (streaming)                  (Postgres)
      │                           │                             │
  User types in      ───────>     │            ────────>    generate()
  CopilotKit                      │                             │
  textarea                        │                             │
      │                           │                             │
  Agent response     <───────  Streaming       <────────    Tools
  (real-time)                  response                     executed

Key Points:

  • useCoAgent hook provides bidirectional state sync
  • Agent state updates trigger React re-renders
  • User input streams to agent via CopilotKit runtime
  • Agent responses stream back to frontend in real-time

🔐 Authentication & Security

Dominad uses Supabase Auth for user authentication with email/password and middleware-based session management.

Authentication Flow

┌─────────────────────────────────────────────────────────────────┐
│                     Authentication Flow                         │
└─────────────────────────────────────────────────────────────────┘

1. USER SIGNUP/LOGIN
   ┌────────────┐
   │ Login Page │ (/login)
   └──────┬─────┘


   supabase.auth.signInWithPassword()
   supabase.auth.signUp()


   ┌──────────────┐
   │  Supabase    │
   │  Auth API    │
   └──────┬───────┘


   Set auth cookies (sb-access-token, sb-refresh-token)


   Redirect to /dashboard/competitors

2. SESSION MANAGEMENT (Every Request)
   ┌────────────┐
   │  Browser   │
   └──────┬─────┘
          │ Request with cookies

   ┌──────────────┐
   │  Middleware  │ (middleware.ts)
   └──────┬───────┘


   updateSession(request)
   - Verify auth cookies
   - Refresh token if expired
   - Update cookies

          ├──> If authenticated: Continue to route

          └──> If not authenticated: Redirect to /login

3. PROTECTED ROUTES
   ┌────────────────┐
   │  Page/API Route│
   └────────┬───────┘


   const supabase = await createClient()
   const { data: { user } } = await supabase.auth.getUser()

            ├──> If user exists: Access granted

            └──> If no user: Return 401 Unauthorized

4. USER-SCOPED DATA
   ┌────────────────┐
   │  Repository    │
   └────────┬───────┘


   All queries include userId filter:
   db.select().from(competitors).where(eq(competitors.userId, userId))


   User can only access their own data

Supabase Client Utilities

Server-Side Client

Location: src/lib/supabase/server.ts

import { createClient } from '@/lib/supabase/server';

// In Server Components, Route Handlers, Server Actions
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();

if (!user) {
  return Response.json({ error: "Unauthorized" }, { status: 401 });
}

Features:

  • Uses @supabase/ssr for cookie management
  • Automatically handles cookie refresh
  • Works in Server Components and API routes

Client-Side Client

Location: src/lib/supabase/client.ts

import { createBrowserClient } from '@/lib/supabase/client';

// In Client Components
const supabase = createBrowserClient();
const { data: { user } } = await supabase.auth.getUser();

Middleware

Location: middleware.ts

import { updateSession } from '@/lib/supabase/middleware';

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

Responsibilities:

  • Verify authentication cookies on every request
  • Refresh expired tokens automatically
  • Update cookies with new tokens
  • Redirect unauthenticated users to /login

Protected Paths:

  • All routes except: /_next/static, /_next/image, /favicon.ico, static assets

User-Scoped Data Isolation

All user data is isolated by userId foreign keys:

// Example: Competitors are user-scoped
export const competitors = pgTable("competitors", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull(), // References auth.users(id)
  name: text("name").notNull(),
  // ...
});

// Repository enforces user scope
async getAll(userId: string): Promise<Competitor[]> {
  return db
    .select()
    .from(competitors)
    .where(eq(competitors.userId, userId));
}

User-Scoped Tables:

  • competitors (userId)
  • analysis_tasks (userId)
  • reports (userId)

Indirectly Scoped (via competitor relationship):

  • ad_creativescompetitorsuserId
  • ad_variationsad_creativescompetitorsuserId
  • ad_region_statsad_creativescompetitorsuserId

Protected API Routes

// Example: /api/competitors/route.ts
export async function GET(request: Request) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const competitors = await competitorsRepository.getAll(user.id);
  return Response.json(competitors);
}

Security Pattern:

  1. Create Supabase client
  2. Get authenticated user
  3. Return 401 if no user
  4. Pass user.id to repository methods
  5. Repository filters by userId

Login/Signup Pages

Location: src/app/(auth)/login/page.tsx

Features:

  • Email/password authentication
  • Form validation with react-hook-form + Zod
  • Error handling for invalid credentials
  • Redirect to /dashboard/competitors on success
  • Email confirmation required for new signups

Environment Variables:

  • SUPABASE_URL: Supabase project URL
  • SUPABASE_ANON_KEY: Supabase anonymous key (public)

🔌 API Routes

Dominad provides a comprehensive REST API for managing competitors, ads, reports, and analysis tasks.

API Route Structure

/api/
├── competitors/
│   ├── GET      - List all competitors for user
│   ├── POST     - Create new competitor
│   ├── [id]/
│   │   ├── GET     - Get competitor by ID
│   │   ├── PATCH   - Update competitor
│   │   └── DELETE  - Delete competitor

├── ads/
│   ├── GET      - List ads with filters (competitorId, platform, format)
│   ├── POST     - Create ad (internal use)
│   ├── [id]/
│   │   ├── GET     - Get ad by ID
│   │   └── analysis/
│   │       └── GET - Get analysis for ad (from ad_insights)

├── reports/
│   ├── GET      - List reports with filters
│   ├── POST     - Create new report
│   ├── [id]/
│   │   ├── GET     - Get report by ID
│   │   └── DELETE  - Delete report

├── scrape-google-ads/
│   └── POST     - Trigger Apify scraping for competitor

├── analyze-video-ad/
│   └── POST     - Analyze video ad with Gemini

├── analyze-image-ad/
│   └── POST     - Analyze image ad with GPT-4o Vision

├── analyze-text-ad/
│   └── POST     - Analyze text ad with GPT-4o

├── analyze-image-text-ads/
│   └── POST     - Batch analyze image/text ads with Mastra agent

├── analyze-landing-page/
│   └── POST     - Analyze landing page with Firecrawl + GPT-4o

├── analysis-tasks/
│   ├── GET      - List analysis tasks
│   ├── POST     - Create analysis task
│   ├── [id]/
│   │   ├── GET     - Get task by ID
│   │   └── PATCH   - Update task status/progress

├── ad-insights/
│   └── GET      - Get insights for competitor

├── proxy-image/
│   └── GET      - CORS proxy for Google Ads images

├── copilotkit/
│   └── POST     - CopilotKit runtime endpoint (Mastra canvas agent)

└── log-client-error/
    └── POST     - Log client-side errors to server

Key API Endpoints

POST /api/scrape-google-ads

Scrapes Google Ads for a competitor using Apify.

Request Body:

{
  "competitorId": "comp_123",
  "googleAdvertiserId": "AR12345678901234567890",
  "countries": ["US", "GB"],
  "languages": ["en"]
}

Response:

{
  "success": true,
  "message": "Scraped 15 ads",
  "creatives": 10,
  "variations": 25,
  "regionStats": 30
}

Process:

  1. Call Apify Google Ads Scraper API
  2. Transform raw Apify data with apify-transformer.ts
  3. Deduplicate ads by creative ID and advertiser ID
  4. Insert into ad_creatives, ad_variations, ad_region_stats
  5. Update competitor's lastAnalysisDate

Location: src/app/api/scrape-google-ads/route.ts

POST /api/analyze-video-ad

Analyzes a video ad using Google Gemini.

Request Body:

{
  "adId": "ad_123",
  "competitorId": "comp_123",
  "videoUrl": "https://youtube.com/watch?v=..."
}

Response:

{
  "success": true,
  "adId": "ad_123",
  "analysis": {
    "hook": { "description": "...", "effectiveness": "high", "techniques": [...] },
    "cta": { "type": "...", "placement": "...", "urgency": "..." },
    "messaging": { "mainMessage": "...", "tone": "...", "benefits": [...] },
    "visualElements": { "style": "...", "pacing": "...", "quality": "..." },
    "targetAudience": { "demographic": "...", "psychographic": "..." },
    "overallEffectiveness": { "score": 8.5, "strengths": [...], "improvements": [...] }
  }
}

Process:

  1. Download YouTube video using youtubei.js
  2. Upload video to Gemini API
  3. Analyze with structured prompt for hooks, CTAs, messaging, visuals
  4. Store result in ad_insights.aiAnalysis
  5. Update ad_creatives.status to mark as analyzed

Location: src/app/api/analyze-video-ad/route.ts

Model: Google Gemini 1.5 Flash (multimodal)

POST /api/analyze-image-text-ads

Batch analyzes image and text ads using Mastra agent.

Request Body:

{
  "competitorId": "comp_123",
  "competitorName": "Acme Corp",
  "filters": {
    "startDate": "2024-01-01",
    "endDate": "2024-12-31",
    "platforms": ["YouTube", "Google Search"],
    "formats": ["IMAGE", "TEXT"]
  }
}

Response:

{
  "success": true,
  "reportId": "report_123",
  "report": {
    "imageAdsAnalyzed": 10,
    "textAdsAnalyzed": 15,
    "imageInsights": { "commonPatterns": [...], "visualThemes": [...] },
    "textInsights": { "messagingPatterns": [...], "keywordAnalysis": [...] },
    "crossFormatComparison": { "effectiveness": "...", "recommendations": [...] },
    "keyInsights": ["Insight 1", "Insight 2", ...],
    "recommendations": ["Recommendation 1", ...]
  }
}

Process:

  1. Fetch filtered ads from database
  2. Separate into image ads and text ads
  3. Call Mastra imageTextReportAgent with ads in working memory
  4. Agent uses imageAdAnalysis and textAdAnalysis tools
  5. Agent aggregates results and identifies patterns
  6. Store report in reports table with reportAds junction entries
  7. Return report ID and full results

Location: src/app/api/analyze-image-text-ads/route.ts

Agent: imageTextReportAgent (Mastra)

GET /api/ads?competitorId=X&platform=Y&format=Z

Lists ads with optional filters.

Query Parameters:

  • competitorId (required): Filter by competitor
  • platform (optional): Filter by platform (YouTube, Google Search, etc.)
  • format (optional): Filter by format (VIDEO, IMAGE, TEXT)
  • limit (optional): Limit results (default: 100)
  • offset (optional): Pagination offset

Response:

{
  "ads": [
    {
      "id": "CR123",
      "competitorId": "comp_123",
      "format": "VIDEO",
      "platform": "YouTube",
      "videoUrl": "https://...",
      "firstSeenDate": "2024-01-15",
      "impressionsLowerBound": 10000,
      "regions": ["US", "GB"],
      "variations": [
        { "headline": "...", "description": "..." }
      ],
      "regionStats": [
        { "regionCode": "US", "impressionsLowerBound": 5000 }
      ]
    }
  ],
  "total": 25
}

Location: src/app/api/ads/route.ts

GET /api/proxy-image?url=X

CORS proxy for Google Ads images.

Purpose: Google Ads images have CORS restrictions that prevent direct loading in the browser. This endpoint proxies the image and adds appropriate CORS headers.

Query Parameters:

  • url: Full URL of the image to proxy

Response: Image binary with CORS headers

Location: src/app/api/proxy-image/route.ts

Usage:

// Instead of:
<img src="https://googleadservices.com/..." /> // CORS error

// Use:
<img src="/api/proxy-image?url=https://googleadservices.com/..." />

POST /api/copilotkit

CopilotKit runtime endpoint for AI canvas.

Purpose: Handles streaming communication between CopilotKit frontend and Mastra canvas agent.

Request: CopilotKit protocol (managed by SDK)

Response: Streaming agent responses

Location: src/app/api/copilotkit/route.ts

Integration:

// Frontend (canvas page)
<CopilotKit runtimeUrl="/api/copilotkit">
  <CoAgentStateRender name="sample_agent" />
</CopilotKit>

Authentication

All API routes (except /api/copilotkit and /api/proxy-image) require authentication:

const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();

if (!user) {
  return Response.json({ error: "Unauthorized" }, { status: 401 });
}

Error Handling

All API routes follow consistent error response format:

{
  "error": "Error message",
  "details": "Optional detailed error information"
}

Common Status Codes:

  • 200: Success
  • 201: Created
  • 400: Bad request (validation error)
  • 401: Unauthorized (no auth or invalid session)
  • 404: Not found
  • 500: Internal server error

🧩 Component Architecture

Dominad uses React 19 with Next.js 15 App Router and shadcn/ui for the component library.

Component Hierarchy

┌─────────────────────────────────────────────────────────────────┐
│                      Component Hierarchy                        │
└─────────────────────────────────────────────────────────────────┘

app/
├── layout.tsx (Root Layout)
│   └── Providers (Theme, Supabase)

├── page.tsx (AI Canvas - Home)
│   └── CopilotKit Provider
│       └── AI Canvas
│           ├── Canvas Header
│           ├── Card Grid
│           │   ├── ProjectCard
│           │   ├── EntityCard
│           │   ├── NoteCard
│           │   ├── ChartCard
│           │   └── GoogleAdCard
│           └── CopilotKit Textarea

└── dashboard/
    └── layout.tsx (Dashboard Layout)
        ├── Sidebar Navigation
        │   ├── Logo
        │   ├── Nav Links (Competitors, Reports)
        │   └── User Menu
        └── Main Content Area
            ├── competitors/
            │   ├── page.tsx (Competitors List)
            │   │   ├── Page Header
            │   │   ├── Actions (Scrape, New Analysis, Add)
            │   │   └── CompetitorsTable
            │   │       ├── Data Table (shadcn/ui)
            │   │       ├── Row Actions (Edit, Delete, View)
            │   │       └── Pagination
            │   └── [id]/
            │       └── page.tsx (Competitor Detail)
            │           ├── Competitor Header
            │           ├── Stats Cards
            │           ├── Tabs (Overview, Ads, Reports)
            │           ├── AdTimeline
            │           │   ├── Filter Controls
            │           │   ├── Timeline View (by date)
            │           │   └── Ad Cards
            │           │       ├── Video Ad Card
            │           │       ├── Image Ad Card
            │           │       └── Text Ad Card
            │           └── CopilotSidebar
            │               └── Analysis Actions
            └── reports/
                ├── page.tsx (Reports List)
                │   ├── Page Header
                │   ├── Filters (Type, Status, Date)
                │   └── ReportsTable
                │       ├── Data Table
                │       ├── Row Actions (View, Delete)
                │       └── Pagination
                └── [id]/
                    └── page.tsx (Report Detail)
                        ├── Report Header
                        ├── Filters Applied
                        ├── Key Insights
                        ├── Analysis Results
                        └── Export Actions

Key Components

Dashboard Components

Location: src/components/dashboard/

CompetitorsTable (CompetitorsTable.tsx)

Data table for competitor listing with sorting, filtering, and actions.

Features:

  • Sortable columns (name, status, googleAdvertiserId, lastAnalysisDate)
  • Search by name or website
  • Status filter (Active, Inactive, Monitoring)
  • Row actions: Edit, Delete, View Details
  • Pagination with configurable page size
  • Row selection for bulk operations

Tech: TanStack Table + shadcn/ui DataTable

AdTimeline (competitor-detail/AdTimeline.tsx)

Timeline view of competitor's ad history with filtering and analysis actions.

Features:

  • Filter by platform (YouTube, Google Search, Display Network)
  • Filter by format (VIDEO, IMAGE, TEXT)
  • Filter by date range
  • Group ads by date
  • Ad card variations (video, image, text)
  • Inline analysis triggers
  • Thumbnail previews with proxy support
  • View counts for video ads

Structure:

AdTimeline
├── FilterControls
│   ├── PlatformSelect
│   ├── FormatSelect
│   └── DateRangePicker
├── TimelineView
│   ├── DateGroup (grouped by date)
│   │   └── AdCard[]
│   │       ├── VideoAdCard
│   │       ├── ImageAdCard
│   │       └── TextAdCard
CopilotSidebar (CopilotSidebar.tsx)

Floating sidebar with AI-powered analysis actions using CopilotKit.

Features:

  • Bulk ad analysis trigger
  • Component-specific analysis (hooks, CTAs, messaging)
  • Landing page analysis
  • Integration with Mastra agents
  • Real-time analysis progress
  • Export results

Tech: CopilotKit + Mastra agents

ReportsTable (ReportsTable.tsx)

Data table for analysis reports with filtering and export.

Features:

  • Filter by type (Bulk Analysis, Component Analysis, Landing Page Analysis)
  • Filter by status (Draft, Published, Archived)
  • Sort by date, name, type
  • View report details
  • Delete reports
  • Export to JSON

Canvas Components

Location: src/components/canvas/

Card Renderers

Individual card types for AI canvas:

  • ProjectCard: Project planning cards
  • EntityCard: Competitor entity cards
  • NoteCard: Text notes and observations
  • ChartCard: Data visualization cards
  • GoogleAdCard: Google Ads preview with character limits

Tech: motion (Framer Motion) for animations

Canvas Controls
  • CanvasHeader: Title, description, global actions
  • CardMenu: Right-click context menu for cards
  • CardActions: Inline card actions (edit, delete, duplicate)

UI Components

Location: src/components/ui/

shadcn/ui component library:

  • button: Button variants (default, outline, ghost, destructive)
  • input, textarea: Form inputs
  • dialog: Modal dialogs
  • dropdown-menu: Dropdown menus
  • table: Data tables
  • select: Select dropdowns
  • checkbox, radio-group: Form controls
  • card: Container cards
  • tabs: Tab navigation
  • progress: Progress bars
  • tooltip: Tooltips
  • alert-dialog: Confirmation dialogs
  • separator: Dividers

Installation: npx shadcn@latest add [component]

State Management

Context API

  • Supabase Context: SupabaseProvider for auth state
  • Theme Context: ThemeProvider for dark/light mode

Hooks

  • useCoAgent: CopilotKit hook for AI agent state synchronization
  • useAuth: Custom hook for Supabase auth (wraps supabase.auth)
  • useDebounce: Debounce hook for search inputs

React Query (Optional)

Not currently used, but could be added for server state management.

Styling

  • Tailwind CSS: Utility-first CSS framework
  • CSS Variables: Theme customization in globals.css
  • Dark Mode: next-themes with system preference detection
  • Responsive Design: Mobile-first with breakpoints (sm, md, lg, xl)

Breakpoints:

  • sm: 640px (mobile)
  • md: 768px (tablet)
  • lg: 1024px (desktop)
  • xl: 1280px (large desktop)

🌐 External Integrations

Dominad integrates with multiple external services for scraping, AI analysis, and data enrichment.

Supabase (Database + Auth)

Purpose: PostgreSQL database and authentication provider

Features Used:

  • Supabase Database: PostgreSQL with Drizzle ORM
  • Supabase Auth: Email/password authentication with session management
  • Row Level Security: User-scoped data isolation (not currently used, handled in app layer)

Configuration:

SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_ANON_KEY=eyJhbGc...
DATABASE_URL=postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres

Documentation: https://supabase.com/docs

Apify (Google Ads Scraping)

Purpose: Scrape Google Ads campaigns from Google Transparency Center

Actor: curious_coder/google-ads-scraper

Features:

  • Scrape ads by Google Advertiser ID
  • Filter by countries, languages, platforms
  • Returns creatives, variations, region stats
  • Supports VIDEO, IMAGE, TEXT ad formats

Usage:

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_API_TOKEN });
const run = await client.actor('curious_coder/google-ads-scraper').call({
  advertiserId: 'AR12345678901234567890',
  countries: ['US', 'GB'],
  languages: ['en'],
});

Configuration:

APIFY_API_TOKEN=apify_api_xxxx

Data Flow: Apify → apify-transformer.ts → Database

Documentation: https://apify.com/docs

OpenAI (GPT-4o)

Purpose: AI analysis for text ads, image ads, and text extraction

Models Used:

  • GPT-4o: Image ad analysis (Vision)
  • GPT-4o-mini: Text ad analysis
  • GPT-4.1: Canvas agent (Mastra)
  • GPT-5: Analysis agent (Mastra)

Use Cases:

  • Analyze image ads for visual elements, text, branding
  • Analyze text ad copy for messaging, CTAs, keywords
  • Power Mastra agents for bulk analysis and reporting
  • Canvas agent for strategy planning

Usage:

import { openai } from '@ai-sdk/openai';

const model = openai('gpt-4o');
const result = await model.generate({
  messages: [{ role: 'user', content: 'Analyze this ad...' }],
});

Configuration:

OPENAI_API_KEY=sk-proj-...

Documentation: https://platform.openai.com/docs

Google Gemini (Video Analysis)

Purpose: Multimodal AI for video ad analysis

Model: Gemini 1.5 Flash

Features:

  • Upload video files directly to Gemini
  • Analyze video content (hooks, CTAs, messaging, visuals)
  • Structured output with JSON schema
  • Multimodal understanding (audio + visual)

Usage:

import { GoogleGenerativeAI } from '@google/genai';

const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });

const result = await model.generateContent([
  { inlineData: { mimeType: 'video/mp4', data: videoBase64 } },
  { text: 'Analyze this video ad...' }
]);

Configuration:

GOOGLE_API_KEY=AIzaSy...

Documentation: https://ai.google.dev/docs

Firecrawl (Landing Page Scraping)

Purpose: Extract structured data from ad landing pages

Features:

  • Scrape web pages and convert to markdown
  • Extract specific data using schemas
  • LLM-ready output format
  • Handles JavaScript-heavy sites

Usage:

import { firecrawlScrapeWithSchema } from '@/mastra/tools/firecrawl-scrape-with-schema';

const result = await firecrawlScrapeWithSchema.execute({
  context: {
    url: 'https://example.com/landing-page',
    schema: {
      type: 'object',
      properties: {
        headline: { type: 'string' },
        cta: { type: 'string' },
        benefits: { type: 'array', items: { type: 'string' } },
      },
    },
  },
});

Configuration:

FIRECRAWL_API_KEY=fc-...

Use Case: Analyze landing page cohesion with ad messaging

Documentation: https://docs.firecrawl.dev/

YouTube (Metadata + Downloads)

Purpose: Fetch video metadata and download videos for analysis

Libraries:

  • youtubei.js: Pure JavaScript YouTube client (download videos, fetch metadata)
  • YouTube Data API v3: Official API for video metadata (fallback)

Features:

  • Download YouTube videos (public, unlisted)
  • Fetch video titles, descriptions, view counts
  • Works in serverless environments (Vercel)
  • Multiple fallback strategies

Configuration:

YOUTUBE_API_KEY=AIzaSy...   # Optional, for Data API v3
YOUTUBE_COOKIES=...         # Optional, for unlisted/private videos

Usage:

import { Innertube } from 'youtubei.js';

const youtube = await Innertube.create();
const info = await youtube.getInfo('VIDEO_ID');
const stream = await info.download();

Documentation: https://github.com/LuanRT/YouTube.js

DataForSEO (Competitor Discovery)

Purpose: Find competitors running Google Ads campaigns

API: Google Ads Transparency API via DataForSEO

Features:

  • Search for advertisers by keyword or URL
  • Get advertiser details (ID, name, website)
  • Find related advertisers

Configuration:

DATAFORSEO_LOGIN=your_login
DATAFORSEO_PASSWORD=your_password

Usage:

import { findCompetitors } from '@/mastra/tools/find-competitors';

const result = await findCompetitors.execute({
  context: { query: 'nike shoes' },
});

Documentation: https://dataforseo.com/apis/google-ads-transparency-api

CopilotKit (AI Canvas)

Purpose: Real-time AI interaction for canvas interface

Features:

  • Bidirectional state synchronization with agents
  • Streaming AI responses
  • Built-in UI components (textarea, suggestions)
  • Mastra integration

Usage:

import { CopilotKit } from '@copilotkit/react-core';
import { useCoAgent } from '@copilotkit/react-core';

<CopilotKit runtimeUrl="/api/copilotkit">
  <MyCanvasComponent />
</CopilotKit>

// In component:
const { agentState } = useCoAgent({ name: 'sample_agent' });

Configuration: No API key required (self-hosted runtime)

Documentation: https://docs.copilotkit.ai


🚀 Getting Started

Prerequisites

Installation

  1. Clone the repository
git clone https://github.com/your-username/dominad.git
cd dominad
  1. Install dependencies
pnpm install
# or: npm install, yarn install, bun install
  1. Set up environment variables
cp .env.example .env

Edit .env and add your API keys:

# Required
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_key
DATABASE_URL=postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres
OPENAI_API_KEY=sk-proj-...
GOOGLE_API_KEY=AIzaSy...

# Optional
APIFY_API_TOKEN=apify_api_...
FIRECRAWL_API_KEY=fc-...
YOUTUBE_API_KEY=AIzaSy...
DATAFORSEO_LOGIN=your_login
DATAFORSEO_PASSWORD=your_password

Where to Get API Keys:

  1. Set up Supabase database

Go to your Supabase project dashboard:

  • SQL Editor → New Query
  • Copy SQL from drizzle/0000_initial_schema.sql (generated by Drizzle)
  • Run the SQL to create tables

Or use Drizzle migration:

pnpm db:push
  1. Generate database types (optional)
pnpm db:generate
  1. Start development server
pnpm dev

Open http://localhost:3000

First-Time Setup

1. Create an Account

  • Navigate to http://localhost:3000/login
  • Click "Sign Up"
  • Enter email and password
  • Check email for confirmation link (if email confirmation enabled in Supabase)

2. Add Your First Competitor

  • Go to "Competitors" page
  • Click "Add Competitor"
  • Enter:
  • Click "Save"

Finding Google Advertiser ID:

  1. Go to https://adstransparency.google.com/
  2. Search for competitor by name or website
  3. Click on advertiser profile
  4. Copy Advertiser ID from URL (format: AR12345678901234567890)

3. Scrape Ads

  • Click on competitor name to view details
  • Click "Scrape Google Ads" button
  • Wait for scraping to complete (may take 30-60 seconds)
  • Ads will appear in the timeline

4. Analyze Ads

  • Click "Analyze" on any ad card
  • Video ads: Analyzed with Gemini
  • Image ads: Analyzed with GPT-4o Vision
  • Text ads: Analyzed with GPT-4o
  • View analysis results in ad card or Reports page

5. Generate Reports

  • Go to "Reports" page
  • Click "Generate Report"
  • Select competitor, date range, filters
  • Click "Generate"
  • View aggregated insights and patterns

Troubleshooting Setup

Database Connection Issues

# Test database connection
pnpm db:studio

If connection fails:

  • Verify DATABASE_URL in .env
  • Check Supabase project is not paused
  • Ensure IP is allowed in Supabase → Settings → Database → Network restrictions

Authentication Issues

# Verify Supabase credentials
echo $SUPABASE_URL
echo $SUPABASE_ANON_KEY

If login fails:

  • Check SUPABASE_URL and SUPABASE_ANON_KEY are correct
  • Verify email confirmation settings in Supabase → Authentication → Settings
  • Check Supabase logs in Dashboard → Logs

API Key Issues

# Test OpenAI key
curl https://api.openai.com/v1/models \
  -H "Authorization: Bearer $OPENAI_API_KEY"

# Test Gemini key
curl "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY"

If API calls fail:

  • Verify API keys are active and have credits
  • Check API key permissions
  • Review API rate limits

🛠️ Development Guide

Project Structure Best Practices

Adding New Features

  1. Create a spec file in specs/ directory
  2. Document the feature with requirements, files to modify, and step-by-step tasks
  3. Implement the feature following the spec
  4. Move spec to specs/done/ after completion

Example: See specs/done/feature-supabase-authentication.md

Modifying Database Schema

  1. Update schema in src/lib/db/schema.ts
export const myTable = pgTable("my_table", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull(),
  name: text("name").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});
  1. Generate migration
pnpm db:generate
  1. Review migration in drizzle/ directory

  2. Apply migration

pnpm db:push
  1. Update repository in src/lib/db/repositories/
export const myRepository = {
  async getAll(userId: string) { ... },
  async create(data: NewMyTable) { ... },
};
  1. Create API route in src/app/api/my-resource/route.ts

Adding API Routes

  1. Create route file: src/app/api/my-route/route.ts

  2. Implement handler:

import { createClient } from '@/lib/supabase/server';
import { myRepository } from '@/lib/db/repositories/my-repository';

export async function GET(request: Request) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const data = await myRepository.getAll(user.id);
  return Response.json(data);
}
  1. Add logging:
import { createLogger } from '@/lib/logger';

const logger = createLogger();

export async function GET(request: Request) {
  const correlationId = crypto.randomUUID();
  logger.info({ correlationId }, 'GET /api/my-route');

  try {
    // ... implementation
    logger.info({ correlationId }, 'Request completed');
  } catch (error) {
    logger.error({ correlationId, error }, 'Request failed');
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

Adding Mastra Agents

  1. Create agent file: src/mastra/agents/my-agent.ts

  2. Define working memory schema:

import { z } from 'zod';

export const MyAgentState = z.object({
  input: z.string(),
  progress: z.number().int().min(0).max(100),
  result: z.object({}).passthrough().optional(),
});
  1. Create agent:
import { Agent } from '@mastra/core/agent';
import { openai } from '@ai-sdk/openai';
import { Memory } from '@mastra/memory';
import { PostgresStore } from '@mastra/pg';

export const myAgent = new Agent({
  name: 'my_agent',
  description: 'Agent description',
  model: openai('gpt-4o'),
  instructions: 'Detailed instructions for agent behavior',
  tools: { /* tools here */ },
  memory: new Memory({
    storage: new PostgresStore({ connectionString: process.env.DATABASE_URL! }),
    options: {
      workingMemory: {
        enabled: true,
        schema: MyAgentState,
      },
    },
  }),
});
  1. Register agent in src/mastra/index.ts:
export const mastra = new Mastra({
  agents: {
    my_agent: myAgent,
  },
});
  1. Use in API route:
import { mastra } from '@/mastra';

const agent = mastra.agents.my_agent;
const result = await agent.generate({
  messages: [{ role: 'user', content: 'Task description' }],
  workingMemory: { input: 'data' },
});

Adding Mastra Tools

  1. Create tool file: src/mastra/tools/my-tool.ts

  2. Define tool:

import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

export const myTool = createTool({
  id: 'my_tool',
  description: 'What this tool does',
  inputSchema: z.object({
    input: z.string(),
  }),
  outputSchema: z.object({
    result: z.string(),
  }),
  execute: async ({ context }) => {
    // Tool implementation
    return { result: 'output' };
  },
});
  1. Export from tools index: src/mastra/tools/index.ts
export { myTool } from './my-tool';
  1. Add to agent:
import { myTool } from '@/mastra/tools/my-tool';

export const myAgent = new Agent({
  tools: { myTool },
});

Adding React Components

  1. Create component: src/components/my-component.tsx

  2. Use TypeScript + React 19:

interface MyComponentProps {
  title: string;
  onAction: () => void;
}

export function MyComponent({ title, onAction }: MyComponentProps) {
  return (
    <div>
      <h2>{title}</h2>
      <button onClick={onAction}>Action</button>
    </div>
  );
}
  1. Use shadcn/ui components:
import { Button } from '@/components/ui/button';
import { Dialog } from '@/components/ui/dialog';

export function MyComponent() {
  return (
    <Dialog>
      <Button>Click me</Button>
    </Dialog>
  );
}
  1. Add to page:
import { MyComponent } from '@/components/my-component';

export default function Page() {
  return <MyComponent title="Hello" onAction={() => {}} />;
}

Logging & Debugging

Logger Configuration

Location: src/lib/logger.ts

Features:

  • Pino logger with correlation IDs
  • Configurable log levels (debug, info, warn, error)
  • JSON or dev format
  • Request grouping to reduce noise
  • Duplicate filtering

Environment Variables:

LOG_LEVEL=debug           # debug | info | warn | error
LOG_FORMAT=dev            # dev | json
LOG_SHOW_CORRELATION=true # Show correlation IDs
LOG_GROUP_REQUESTS=false  # Disable request grouping
LOG_SHOW_DUPLICATES=true  # Show duplicate log entries
LOG_SHOW_ROUTINE=true     # Show routine operations
NO_COLOR=1                # Disable colored output

Usage:

import { createLogger } from '@/lib/logger';

const logger = createLogger();

// Basic logging
logger.info('Message');
logger.warn({ context: 'value' }, 'Warning message');
logger.error({ error }, 'Error message');

// With correlation ID
const correlationId = crypto.randomUUID();
logger.info({ correlationId }, 'Request started');
logger.info({ correlationId }, 'Request completed');

Debug Mode

Start dev server with debug logging:

pnpm run dev:debug

Or set environment variable:

export LOG_LEVEL=debug
pnpm dev

Common Development Tasks

View database in GUI:

pnpm db:studio

Test video analysis locally:

pnpm test:video-analysis

Repair YouTube metadata (for ads scraped as "Untitled Ad"):

pnpm db:repair-untitled

Migrate proxy URLs to original URLs:

pnpm db:migrate-urls

Testing

Manual Testing

Test Video Analysis:

pnpm test:video-analysis

Test Apify Scraping:

  1. Add competitor with Google Advertiser ID
  2. Click "Scrape Google Ads"
  3. Verify ads appear in timeline

Test AI Analysis:

  1. Scrape ads for competitor
  2. Click "Analyze" on ad card
  3. Verify analysis appears in ad insights

API Testing with cURL

Test authentication:

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "password"}'

Test competitor API:

curl http://localhost:3000/api/competitors \
  -H "Cookie: sb-access-token=YOUR_TOKEN"

Common Issues & Solutions

Issue: "Unauthorized" on API requests

Solution: Verify Supabase session cookies are being sent. Check middleware is running.

Issue: Video download fails in production (Vercel)

Solution: Ensure youtubei.js is installed. Check /tmp directory permissions.

Issue: Apify scraping returns 0 ads

Solution: Verify Google Advertiser ID is correct. Some advertisers may not have video ads on YouTube.

Issue: Database migration fails

Solution: Check DATABASE_URL is correct. Ensure Supabase project is active. Review migration SQL in drizzle/ directory.

Issue: Gemini API rate limit exceeded

Solution: Implement retry logic with exponential backoff. Consider using Gemini 1.5 Flash instead of Pro.


📜 Available Scripts

Development Scripts

ScriptDescription
pnpm devStart development server (Next.js + Turbopack)
pnpm dev:debugStart dev server with debug logging (LOG_LEVEL=debug)
pnpm dev:quietStart dev server with minimal logging (LOG_LEVEL=warn)
pnpm dev:verboseStart dev server with verbose JSON logging
pnpm dev:jsonStart dev server with JSON logging format
pnpm dev:agentStart Mastra agent development server (standalone)

Build & Production Scripts

ScriptDescription
pnpm buildBuild production bundle
pnpm startStart production server
pnpm lintRun ESLint for code linting

Database Scripts

ScriptDescription
pnpm db:generateGenerate Drizzle migration files from schema
pnpm db:pushApply migrations to database
pnpm db:studioOpen Drizzle Studio (database GUI)
pnpm db:setupInitialize database (migrations + setup)
pnpm db:repair-untitledRepair "Untitled Ad" entries with YouTube API
pnpm db:migrate-urlsMigrate proxy URLs to original image URLs

Testing Scripts

ScriptDescription
pnpm test:video-analysisTest video analysis with sample YouTube URL

Script Details

pnpm dev

Starts Next.js development server with:

  • Turbopack (faster builds)
  • Hot module replacement
  • LOG_FORMAT=dev (human-readable logs)
  • Default LOG_LEVEL=info

pnpm db:generate

Generates SQL migration files based on schema changes:

  1. Reads src/lib/db/schema.ts
  2. Compares with current database state
  3. Generates SQL diff in drizzle/ directory
  4. Run pnpm db:push to apply

pnpm db:push

Applies migrations to database:

  1. Reads migration files from drizzle/
  2. Executes SQL against DATABASE_URL
  3. Updates migration tracking table

Note: Use db:generate before db:push to create migrations.

pnpm db:studio

Opens Drizzle Studio on http://localhost:4983:

  • Browse all tables
  • View/edit data
  • Explore relationships
  • Run SQL queries

🚢 Deployment

Vercel Deployment

Dominad is optimized for Vercel with serverless functions.

Prerequisites

  • Vercel account (https://vercel.com)
  • Supabase project (production database)
  • API keys for OpenAI, Gemini, etc.

Deployment Steps

  1. Push to GitHub
git add .
git commit -m "Ready for deployment"
git push origin main
  1. Import to Vercel
  1. Configure Environment Variables

Add these in Vercel project settings → Environment Variables:

Required:

SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_key
DATABASE_URL=postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres
OPENAI_API_KEY=sk-proj-...
GOOGLE_API_KEY=AIzaSy...
NEXT_PUBLIC_SITE_URL=https://your-app.vercel.app

Optional:

APIFY_API_TOKEN=apify_api_...
FIRECRAWL_API_KEY=fc-...
YOUTUBE_API_KEY=AIzaSy...
DATAFORSEO_LOGIN=your_login
DATAFORSEO_PASSWORD=your_password
LOG_LEVEL=info
LOG_FORMAT=json
  1. Deploy
  • Click "Deploy"
  • Wait for build to complete
  • Visit your deployment URL
  1. Set up production database

Run migrations on Supabase:

pnpm db:push

Or manually run SQL from drizzle/ directory in Supabase SQL Editor.

Vercel Configuration

Build Settings:

  • Framework: Next.js
  • Build Command: pnpm build
  • Output Directory: .next
  • Install Command: pnpm install
  • Node.js Version: 18.x or higher

Function Configuration (optional vercel.json):

{
  "functions": {
    "src/app/api/analyze-video-ad/route.ts": {
      "maxDuration": 60
    }
  }
}

Serverless Considerations:

  • Function timeout: 10s (Hobby), 60s (Pro), 300s (Enterprise)
  • Memory limit: 1024MB (Hobby), 3008MB (Pro)
  • Disk space: 512MB in /tmp directory
  • Cold starts: First request may be slower

Production Database Setup

Supabase Production Database:

  1. Create production Supabase project
  2. Copy connection string from Settings → Database
  3. Add to Vercel environment variables as DATABASE_URL
  4. Run migrations:
DATABASE_URL=postgresql://... pnpm db:push

Connection Pooling: Supabase provides connection pooling by default. Use transaction mode for serverless:

DATABASE_URL=postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true

Monitoring & Logging

Vercel Logs:

  • Dashboard → Deployments → [Deployment] → Logs
  • Real-time function logs
  • Filter by status (errors, warnings)

Structured Logging: Set LOG_FORMAT=json for production to enable structured JSON logs:

LOG_FORMAT=json
LOG_LEVEL=info

Error Tracking (optional): Integrate Sentry or similar:

npm install @sentry/nextjs

Environment Variables

VariableRequiredDescription
SUPABASE_URLYesSupabase project URL
SUPABASE_ANON_KEYYesSupabase anonymous key
DATABASE_URLYesPostgreSQL connection string
OPENAI_API_KEYYesOpenAI API key (GPT models)
GOOGLE_API_KEYYesGoogle Gemini API key (video analysis)
NEXT_PUBLIC_SITE_URLYesPublic URL of deployment
APIFY_API_TOKENNoApify token (Google Ads scraping)
FIRECRAWL_API_KEYNoFirecrawl API key (landing page scraping)
YOUTUBE_API_KEYNoYouTube Data API key (metadata)
DATAFORSEO_LOGINNoDataForSEO login (competitor discovery)
DATAFORSEO_PASSWORDNoDataForSEO password
LOG_LEVELNoLogging level (default: info)
LOG_FORMATNoLogging format (default: json)

Vercel Edge Functions (Future)

For lower latency, consider migrating API routes to Edge Functions:

  • Runs on Vercel Edge Network (50+ regions)
  • Lower cold start times
  • Limited to Edge-compatible APIs (no Node.js APIs)

Note: Current implementation uses Node.js runtime for youtubei.js compatibility.


🔬 Advanced Topics

Custom Slash Commands

Location: .claude/commands/

Create custom slash commands for Claude Code:

Example (.claude/commands/analyze.md):

Analyze the ad with ID {{adId}} for competitor {{competitorId}} and store results in the database.

Usage: /analyze adId=ad_123 competitorId=comp_456

Logging Infrastructure

Features:

  • Correlation IDs: Track requests across multiple log entries
  • Structured Logging: JSON format for log aggregation tools
  • Request Grouping: Reduce log noise by grouping similar requests
  • Duplicate Filtering: Hide repeated log messages
  • Routine Filtering: Hide routine operations (fetching, loading)

Example:

const correlationId = crypto.randomUUID();
logger.info({ correlationId, userId: user.id }, 'Request started');
// ... do work
logger.info({ correlationId }, 'Request completed', { duration: 123 });

Error Boundaries

Client-Side Error Handling:

Create error.tsx in route groups:

'use client';

export default function Error({ error, reset }: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Global Error Boundary: src/app/error.tsx

Image Proxy System

Purpose: Bypass CORS restrictions for Google Ads images

How it works:

  1. Frontend requests /api/proxy-image?url=https://...
  2. Backend fetches image from Google Ads
  3. Backend returns image with CORS headers

Implementation: src/app/api/proxy-image/route.ts

Usage:

<img src={`/api/proxy-image?url=${encodeURIComponent(imageUrl)}`} />

YouTube Metadata Repair

Script: scripts/repair-untitled-ads.ts

Purpose: Repair ads scraped as "Untitled Ad" by fetching YouTube metadata

Usage:

pnpm db:repair-untitled

How it works:

  1. Find ads with headline "Untitled Ad"
  2. Extract video ID from videoUrl
  3. Fetch title/description from YouTube Data API v3 or youtubei.js
  4. Update ad variations in database

Ad Deduplication Logic

Location: src/lib/dashboard/apify-transformer.ts

Strategy:

  • Deduplicate by creative ID (Apify's unique identifier)
  • For ads without creative ID, deduplicate by advertiser ID + format + platform
  • Skip ads that already exist in database

Implementation:

// Check if ad already exists
const existingAd = await db
  .select()
  .from(adCreatives)
  .where(eq(adCreatives.id, creativeId))
  .limit(1);

if (existingAd.length > 0) {
  logger.info('Skipping duplicate ad', { creativeId });
  continue;
}

Background Task Processing

Current: Synchronous API calls

Future Enhancement: Implement job queue with BullMQ or Inngest:

  1. User triggers analysis
  2. Create task in analysis_tasks table with status "pending"
  3. Enqueue background job
  4. Job worker processes analysis
  5. Update task status to "completed" or "failed"
  6. Frontend polls for task completion

Benefits:

  • Non-blocking API responses
  • Better error handling and retries
  • Progress tracking
  • Scalability

Custom Mastra Workflows

Example: Multi-step competitor analysis workflow

  1. User triggers workflow from UI
  2. Workflow agent:
    • Step 1: Scrape ads (Apify)
    • Step 2: Analyze videos (Gemini)
    • Step 3: Analyze images/text (GPT-4o)
    • Step 4: Generate report (Analysis agent)
  3. Update progress in working memory
  4. Stream results to frontend via CopilotKit

Implementation:

export const workflowAgent = new Agent({
  name: 'workflow_agent',
  tools: { scrapeAds, analyzeAds, generateReport },
  memory: new Memory({ /* ... */ }),
});

📂 Specs Directory Overview

The specs/ directory contains detailed specifications for all features, bug fixes, and chores implemented in Dominad. There are 96 completed specs in specs/done/.

Spec Categories

Features (prefix: feature-*)

New functionality added to the application.

Examples:

  • feature-supabase-authentication.md - Email/password authentication with Supabase
  • feature-ai-video-ad-analysis.md - Video analysis with Google Gemini
  • feature-landing-page-analysis.md - Firecrawl integration for landing page scraping
  • feature-bulk-report-generation.md - Batch analysis with Mastra agents
  • feature-competitor-discovery.md - DataForSEO integration for finding advertisers

Bug Fixes (prefix: bug-*)

Issues resolved and fixes implemented.

Examples:

  • bug-database-migration-libsql-to-supabase.md - Database migration debugging
  • bug-fix-apify-image-cors-proxy.md - CORS proxy for Google Ads images
  • bug-fix-missing-userId-fields.md - User-scoped data isolation fix
  • bug-logout-redirect-error.md - Logout redirect loop fix

Chores (prefix: chore-*)

Maintenance, refactoring, and infrastructure improvements.

Examples:

  • chore-migrate-libsql-to-supabase.md - Database migration from LibSQL to Supabase
  • chore-setup-drizzle-orm.md - Drizzle ORM integration
  • chore-add-logging-infrastructure.md - Pino logger with correlation IDs

Refactors (prefix: refactor-*)

Code improvements and architectural changes.

Examples:

  • refactor-apify-data-flow-normalized-schema.md - Normalized database schema
  • refactor-repository-pattern.md - Repository pattern for data access

Key Specs to Reference

Authentication

  • feature-supabase-authentication.md - Complete auth implementation guide

Database

  • refactor-apify-data-flow-normalized-schema.md - New normalized schema design
  • chore-migrate-libsql-to-supabase.md - Migration to Supabase

AI Analysis

  • feature-ai-video-ad-analysis.md - Gemini video analysis
  • feature-bulk-report-generation.md - Mastra agent batch analysis
  • feature-landing-page-analysis.md - Firecrawl landing page scraping

Scraping

  • apify-google-ads-scraper-integration.md - Apify integration
  • automated-youtube-metadata-repair.md - YouTube metadata repair system

Spec Format

Each spec follows this structure:

# [Type]: [Title]

## Description
What this feature/fix does and why it's needed.

## Relevant Files
- Files to read for context
- Files to modify
- New files to create

## Step by Step Tasks
1. Task 1
2. Task 2
...

## Validation Commands
Commands to verify the implementation.

## Notes
Additional context, warnings, or considerations.

Using Specs

As a Developer:

  • Read specs before implementing features
  • Follow step-by-step tasks in order
  • Run validation commands after completion
  • Move spec to specs/done/ when done

As a Reference:

  • Understand how features were built
  • Learn about architectural decisions
  • Find examples for new implementations

🧰 Tech Stack

Frontend

  • Framework: Next.js 15 (App Router, React Server Components)
  • Language: TypeScript 5.9
  • UI Library: React 19
  • Component Library: shadcn/ui (Radix UI + Tailwind CSS)
  • Styling: Tailwind CSS 4
  • State Management: Context API, React hooks
  • AI Integration: CopilotKit (real-time AI canvas)
  • Animations: Framer Motion (via motion package)
  • Forms: react-hook-form + Zod validation
  • Tables: TanStack Table

Backend

  • Runtime: Node.js 18+
  • Framework: Next.js API Routes (App Router)
  • Language: TypeScript 5.9
  • AI Agents: Mastra 0.15 (agent framework)
  • LLMs: OpenAI GPT-4o, GPT-5, Google Gemini 1.5 Flash
  • Logging: Pino (structured JSON logs)

Database

  • Database: Supabase (PostgreSQL 15)
  • ORM: Drizzle ORM 0.44
  • Schema: Normalized relational schema
  • Migrations: Drizzle Kit
  • Type Safety: TypeScript types inferred from schema

Authentication

  • Provider: Supabase Auth
  • Method: Email/password
  • Session Management: Cookie-based with middleware
  • Integration: @supabase/ssr

External APIs

  • Scraping: Apify (Google Ads Transparency Center)
  • AI Analysis: OpenAI (GPT-4o, GPT-5), Google Gemini
  • Landing Pages: Firecrawl (web scraping)
  • YouTube: youtubei.js (video download), YouTube Data API v3 (metadata)
  • Competitor Discovery: DataForSEO (Google Ads Transparency API)

Development Tools

  • Package Manager: pnpm
  • Linting: ESLint 9 (Next.js config)
  • Type Checking: TypeScript strict mode
  • Dev Server: Next.js with Turbopack
  • Database GUI: Drizzle Studio

Deployment

  • Platform: Vercel (serverless functions)
  • CDN: Vercel Edge Network
  • Database: Supabase (managed PostgreSQL)
  • Environment: Node.js 18+ runtime

Key Dependencies

PackageVersionPurpose
next15.3.3React framework
react19.1.1UI library
typescript5.9.2Type safety
drizzle-orm0.44.6Database ORM
@supabase/supabase-js2.75.0Supabase client
@mastra/core0.20.2AI agent framework
@copilotkit/react-core1.10.6AI canvas integration
@ai-sdk/openai2.0.50OpenAI integration
@google/genai1.24.0Gemini integration
apify-client2.18.0Apify scraping
youtubei.js16.0.1YouTube downloads
tailwindcss4.1.12CSS framework
lucide-react0.542.0Icon library
zod3.25.76Schema validation
pino10.0.0Logging

🛠️ Troubleshooting

Common Issues

Authentication Issues

Problem: "Unauthorized" error on dashboard pages

Solutions:

  1. Check Supabase credentials in .env:
    echo $SUPABASE_URL
    echo $SUPABASE_ANON_KEY
    
  2. Verify middleware is running (check middleware.ts)
  3. Clear browser cookies and login again
  4. Check Supabase project is active (not paused)
  5. Verify email is confirmed (if email confirmation enabled)

Problem: Login redirects to login page infinitely

Solution: Check middleware.ts redirect logic. Ensure /login is excluded from auth checks.


Database Issues

Problem: "Failed to connect to database"

Solutions:

  1. Verify DATABASE_URL is correct:
    echo $DATABASE_URL
    
  2. Check Supabase project is active
  3. Test connection with Drizzle Studio:
    pnpm db:studio
    
  4. Verify IP whitelist in Supabase → Settings → Database

Problem: Migration fails with "relation already exists"

Solutions:

  1. Drop existing tables in Supabase SQL Editor
  2. Re-run migration:
    pnpm db:push
    
  3. Or apply migration manually from drizzle/ directory

Problem: "Type error: Cannot find module '@/lib/db/schema'"

Solution: Restart TypeScript server in your editor.


Video Analysis Issues

Problem: Video download fails in production (Vercel)

Solutions:

  1. Ensure youtubei.js is installed:
    pnpm install youtubei.js
    
  2. Check video URL is valid YouTube URL
  3. Verify /tmp directory has space (512MB limit on Vercel)
  4. Review Vercel function logs for errors

Problem: Gemini API rate limit exceeded

Solutions:

  1. Implement exponential backoff retry logic
  2. Switch to Gemini 1.5 Flash (cheaper, faster)
  3. Upgrade Gemini API quota
  4. Add delay between video analysis requests

Problem: Video analysis returns "Invalid video format"

Solutions:

  1. Verify video URL is YouTube URL (not other platforms)
  2. Check video is public (not private/unlisted)
  3. Try with YOUTUBE_COOKIES for unlisted videos

API Issues

Problem: "API key invalid" errors

Solutions:

  1. Verify API keys are active:
    curl https://api.openai.com/v1/models \
      -H "Authorization: Bearer $OPENAI_API_KEY"
    
  2. Check API keys have sufficient credits
  3. Verify API keys in .env match Vercel environment variables
  4. Regenerate API keys if expired

Problem: Apify scraping returns 0 ads

Solutions:

  1. Verify Google Advertiser ID is correct (format: AR...)
  2. Check advertiser has active ads on selected platforms
  3. Try different target countries/languages
  4. Review Apify actor logs in Apify console

Problem: "Rate limit exceeded" from OpenAI/Gemini

Solutions:

  1. Implement request queuing with delays
  2. Upgrade API tier for higher rate limits
  3. Add exponential backoff retry logic
  4. Cache analysis results to avoid re-analyzing

Image Issues

Problem: Images not loading (CORS error)

Solution: Use image proxy:

<img src={`/api/proxy-image?url=${encodeURIComponent(imageUrl)}`} />

Problem: Image proxy returns 404

Solutions:

  1. Verify image URL is correct
  2. Check image URL is accessible externally
  3. Review proxy endpoint logs
  4. Ensure NEXT_PUBLIC_SITE_URL is set correctly

Build Issues

Problem: TypeScript errors during build

Solutions:

  1. Run type check:
    pnpm tsc --noEmit
    
  2. Fix type errors in reported files
  3. Ensure all imports are correct
  4. Restart TypeScript server

Problem: "Module not found" errors

Solutions:

  1. Verify dependencies are installed:
    pnpm install
    
  2. Check import paths are correct (use @/ for src/)
  3. Restart dev server
  4. Clear .next cache:
    rm -rf .next
    pnpm dev
    

Debug Mode

Enable verbose logging:

LOG_LEVEL=debug pnpm dev

Enable JSON logging for structured logs:

LOG_FORMAT=json LOG_LEVEL=debug pnpm dev

Show all log details:

LOG_SHOW_CORRELATION=true \
LOG_SHOW_DUPLICATES=true \
LOG_SHOW_ROUTINE=true \
pnpm dev

Getting Help


📄 License

This project is licensed under the MIT License. See LICENSE file for details.


🤝 Contributing

Contributions are welcome! This project is in active development.

How to Contribute

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m "Add my feature"
  4. Push to the branch: git push origin feature/my-feature
  5. Open a Pull Request

Contribution Guidelines

  • Follow existing code style and conventions
  • Write clear commit messages
  • Add tests for new features (when test infrastructure is added)
  • Update documentation for new features
  • Create a spec file in specs/ for significant changes

🗺️ Roadmap

Upcoming Features

  • WebSocket Support: Real-time ad updates and live scraping progress
  • Report Templates: Customizable report formats with export to PDF/Excel
  • Email Notifications: Alert users when competitor launches new ads
  • Ad Performance Metrics: Track impressions over time, estimate budgets
  • Multi-Platform Scraping: Support for Meta Ads, LinkedIn Ads, TikTok Ads
  • Team Collaboration: Multi-user workspaces with role-based access
  • API for Integrations: RESTful API for external tools
  • Chrome Extension: Capture ads while browsing
  • Automated Reporting: Schedule reports to run automatically

Planned Improvements

  • Enhanced AI Analysis: Sentiment analysis, competitor positioning
  • Historical Ad Tracking: Track ad changes over time
  • Competitor Comparison: Side-by-side ad comparison views
  • Bulk Operations: Batch delete, batch analyze, batch export
  • Advanced Filters: Complex filter combinations with AND/OR logic
  • Search Functionality: Full-text search across ads and reports
  • Dashboard Analytics: Charts, graphs, trend visualizations

Built with ❤️ using Next.js, Mastra, CopilotKit, and Supabase

Report BugRequest FeatureDocumentation