Dominad
October 19, 2025 · View on GitHub
AI-powered competitive intelligence platform for Google Ads advertisers
Built with Next.js, Mastra, CopilotKit, and Supabase
Features • Architecture • Getting Started • API Docs • Development
📑 Table of Contents
- Overview
- Key Features
- System Architecture
- Database Architecture
- AI Agent System
- Authentication & Security
- API Routes
- Component Architecture
- External Integrations
- Getting Started
- Development Guide
- Available Scripts
- Deployment
- Advanced Topics
- Specs Directory
- Tech Stack
- Troubleshooting
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
- Scrapes competitor Google Ads via Apify (YouTube video ads, text ads, image ads)
- Analyzes ad creatives with AI using GPT-4o for text/images and Gemini for videos
- Tracks ad campaigns with normalized database schema for variations and regional stats
- Generates strategic reports identifying patterns, themes, and opportunities
- Provides AI canvas for strategy planning and campaign ideation with CopilotKit
- 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.
| Column | Type | Description |
|---|---|---|
id | text | Primary key (nanoid) |
userId | text | Foreign key to auth.users (user-scoped data) |
name | text | Competitor business name |
website | text | Full website URL |
domain | text | Domain name (e.g., "example.com") |
googleAdvertiserId | text | Google Advertiser ID for scraping |
status | text | Active, Inactive, Monitoring |
targetCountries | jsonb | Array of ISO country codes |
targetLanguages | jsonb | Array of language codes |
niche | text | Business niche/category |
industry | text | Industry classification |
Indexes: userId, googleAdvertiserId, status
ad_creatives (NEW - Normalized Schema)
Stores unique ad creatives from Google Ads Transparency Center.
| Column | Type | Description |
|---|---|---|
id | text | Apify creative ID (e.g., "CR10435096316668280833") |
competitorId | text | Foreign key to competitors |
advertiserId | text | Apify advertiser ID |
advertiserName | text | Advertiser name |
format | text | TEXT, IMAGE, VIDEO |
platform | text | YouTube, Google Search, Display Network |
videoUrl | text | YouTube URL (for VIDEO ads) |
previewUrl | text | Ad preview URL |
firstSeenDate | text | ISO date (earliest across all regions) |
lastSeenDate | text | ISO date (most recent across all regions) |
impressionsLowerBound | integer | Total impressions lower bound |
impressionsUpperBound | integer | Total impressions upper bound |
views | integer | YouTube view count (nullable) |
regions | jsonb | Array of region codes where ad is shown |
status | text | Active, 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.
| Column | Type | Description |
|---|---|---|
id | text | Composite: ${creative_id}-${hash} |
creativeId | text | Foreign key to ad_creatives |
headline | text | Ad headline |
description | text | Ad description |
clickThroughUrl | text | Landing page URL |
imageUrl | text | Thumbnail or logo URL |
targetKeywords | jsonb | Array of keywords |
firstSeenDate | text | When variation first appeared |
lastSeenDate | text | When variation last seen |
status | text | Active, Paused, Removed |
Indexes: creativeId, status, unique index on (creativeId, headline, description)
ad_region_stats (NEW - Normalized Schema)
Time-series data for regional impression tracking.
| Column | Type | Description |
|---|---|---|
id | text | nanoid |
creativeId | text | Foreign key to ad_creatives |
regionCode | text | ISO country code (FR, US, BE, etc.) |
regionName | text | Full region name |
firstShown | text | Date first shown in region (YYYY-MM-DD) |
lastShown | text | Date last shown in region (YYYY-MM-DD) |
impressionsLowerBound | integer | Impressions lower bound |
impressionsUpperBound | integer | Impressions upper bound |
snapshotDate | text | Date of this snapshot (ISO) |
surfaceStats | jsonb | Platform-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.
| Column | Type | Description |
|---|---|---|
id | text | Primary key |
competitorId | text | Foreign key to competitors |
metricName | text | Metric identifier |
value | real | Current metric value |
previousValue | real | Previous metric value |
trend | text | up, down, stable |
period | text | Time period (e.g., "last-30-days") |
category | text | ai-marketing-analysis, frequency, messaging |
aiAnalysis | jsonb | Video analysis results from Gemini |
imageAnalysis | jsonb | Image analysis results from GPT-4o |
textAnalysis | jsonb | Text analysis results from GPT-4o |
landingPageAnalysis | jsonb | Landing page analysis from Firecrawl + GPT-4o |
Indexes: competitorId, category, period
reports
AI-generated competitive analysis reports.
| Column | Type | Description |
|---|---|---|
id | text | Primary key |
userId | text | Foreign key to auth.users |
competitorId | text | Foreign key to competitors |
type | text | Bulk Analysis, Component Analysis, Landing Page Analysis |
analysisType | text | bulk-ads, component-specific, landing-page-full |
filters | jsonb | Filter options used (date range, platform, format) |
results | jsonb | Analysis results |
insights | jsonb | Key insights array |
metadata | jsonb | Additional metadata |
status | text | Draft, Published, Archived |
Indexes: userId, competitorId, type, analysisType, date
report_ads (Junction Table)
Many-to-many relationship between reports and ads.
| Column | Type | Description |
|---|---|---|
id | text | Primary key |
reportId | text | Foreign key to reports |
adId | text | Foreign 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):
adstable 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 creativead_variations: Multiple rows for different headline/description combinationsad_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 planupdatePlanProgress: Update plan step statuscompletePlan: Mark plan as completedfindCompetitors: 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 VisiontextAdAnalysis: Analyze text ad copy with GPT-4o
Model: OpenAI GPT-4o
Workflow:
- Analyze each image ad individually using vision tool
- Analyze each text ad individually using text tool
- Aggregate format-specific insights
- 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 stepsupdatePlanProgress: Update step status (pending, in_progress, completed, blocked, failed)completePlan: Mark plan as done
Analysis Tools
imageAdAnalysis: Extract visual elements, text, branding from imagestextAdAnalysis: Analyze headlines, descriptions, CTAs, messagingfirecrawlScrapeWithSchema: Scrape landing pages with structured schemafindCompetitors: 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:
useCoAgenthook 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/ssrfor 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_creatives→competitors→userIdad_variations→ad_creatives→competitors→userIdad_region_stats→ad_creatives→competitors→userId
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:
- Create Supabase client
- Get authenticated user
- Return 401 if no user
- Pass
user.idto repository methods - 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/competitorson success - Email confirmation required for new signups
Environment Variables:
SUPABASE_URL: Supabase project URLSUPABASE_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:
- Call Apify Google Ads Scraper API
- Transform raw Apify data with
apify-transformer.ts - Deduplicate ads by creative ID and advertiser ID
- Insert into
ad_creatives,ad_variations,ad_region_stats - 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:
- Download YouTube video using
youtubei.js - Upload video to Gemini API
- Analyze with structured prompt for hooks, CTAs, messaging, visuals
- Store result in
ad_insights.aiAnalysis - Update
ad_creatives.statusto 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:
- Fetch filtered ads from database
- Separate into image ads and text ads
- Call Mastra
imageTextReportAgentwith ads in working memory - Agent uses
imageAdAnalysisandtextAdAnalysistools - Agent aggregates results and identifies patterns
- Store report in
reportstable withreportAdsjunction entries - 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 competitorplatform(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: Success201: Created400: Bad request (validation error)401: Unauthorized (no auth or invalid session)404: Not found500: 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 cardsEntityCard: Competitor entity cardsNoteCard: Text notes and observationsChartCard: Data visualization cardsGoogleAdCard: Google Ads preview with character limits
Tech: motion (Framer Motion) for animations
Canvas Controls
CanvasHeader: Title, description, global actionsCardMenu: Right-click context menu for cardsCardActions: 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 inputsdialog: Modal dialogsdropdown-menu: Dropdown menustable: Data tablesselect: Select dropdownscheckbox,radio-group: Form controlscard: Container cardstabs: Tab navigationprogress: Progress barstooltip: Tooltipsalert-dialog: Confirmation dialogsseparator: Dividers
Installation: npx shadcn@latest add [component]
State Management
Context API
- Supabase Context:
SupabaseProviderfor auth state - Theme Context:
ThemeProviderfor dark/light mode
Hooks
useCoAgent: CopilotKit hook for AI agent state synchronizationuseAuth: Custom hook for Supabase auth (wrapssupabase.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-themeswith 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
- Node.js: 18.x or higher
- Package Manager: pnpm (recommended), npm, yarn, or bun
- Supabase Account: https://supabase.com (free tier available)
- OpenAI API Key: https://platform.openai.com/api-keys
- Google API Key: https://aistudio.google.com/app/apikey (for Gemini)
Installation
- Clone the repository
git clone https://github.com/your-username/dominad.git
cd dominad
- Install dependencies
pnpm install
# or: npm install, yarn install, bun install
- 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:
- Supabase: Create project at https://app.supabase.com → Settings → API
- OpenAI: https://platform.openai.com/api-keys
- Google Gemini: https://aistudio.google.com/app/apikey
- Apify: https://console.apify.com/account/integrations
- Firecrawl: https://www.firecrawl.dev/
- DataForSEO: https://dataforseo.com/
- 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
- Generate database types (optional)
pnpm db:generate
- Start development server
pnpm dev
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:
- Name: Competitor business name
- Website: Full URL (e.g., https://example.com)
- Google Advertiser ID: Find this at https://adstransparency.google.com/
- Target Countries: US, GB, etc.
- Target Languages: en, fr, etc.
- Click "Save"
Finding Google Advertiser ID:
- Go to https://adstransparency.google.com/
- Search for competitor by name or website
- Click on advertiser profile
- 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_URLin.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_URLandSUPABASE_ANON_KEYare 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
- Create a spec file in
specs/directory - Document the feature with requirements, files to modify, and step-by-step tasks
- Implement the feature following the spec
- Move spec to
specs/done/after completion
Example: See specs/done/feature-supabase-authentication.md
Modifying Database Schema
- 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(),
});
- Generate migration
pnpm db:generate
-
Review migration in
drizzle/directory -
Apply migration
pnpm db:push
- Update repository in
src/lib/db/repositories/
export const myRepository = {
async getAll(userId: string) { ... },
async create(data: NewMyTable) { ... },
};
- Create API route in
src/app/api/my-resource/route.ts
Adding API Routes
-
Create route file:
src/app/api/my-route/route.ts -
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);
}
- 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
-
Create agent file:
src/mastra/agents/my-agent.ts -
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(),
});
- 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,
},
},
}),
});
- Register agent in
src/mastra/index.ts:
export const mastra = new Mastra({
agents: {
my_agent: myAgent,
},
});
- 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
-
Create tool file:
src/mastra/tools/my-tool.ts -
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' };
},
});
- Export from tools index:
src/mastra/tools/index.ts
export { myTool } from './my-tool';
- Add to agent:
import { myTool } from '@/mastra/tools/my-tool';
export const myAgent = new Agent({
tools: { myTool },
});
Adding React Components
-
Create component:
src/components/my-component.tsx -
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>
);
}
- 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>
);
}
- 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:
- Add competitor with Google Advertiser ID
- Click "Scrape Google Ads"
- Verify ads appear in timeline
Test AI Analysis:
- Scrape ads for competitor
- Click "Analyze" on ad card
- 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
| Script | Description |
|---|---|
pnpm dev | Start development server (Next.js + Turbopack) |
pnpm dev:debug | Start dev server with debug logging (LOG_LEVEL=debug) |
pnpm dev:quiet | Start dev server with minimal logging (LOG_LEVEL=warn) |
pnpm dev:verbose | Start dev server with verbose JSON logging |
pnpm dev:json | Start dev server with JSON logging format |
pnpm dev:agent | Start Mastra agent development server (standalone) |
Build & Production Scripts
| Script | Description |
|---|---|
pnpm build | Build production bundle |
pnpm start | Start production server |
pnpm lint | Run ESLint for code linting |
Database Scripts
| Script | Description |
|---|---|
pnpm db:generate | Generate Drizzle migration files from schema |
pnpm db:push | Apply migrations to database |
pnpm db:studio | Open Drizzle Studio (database GUI) |
pnpm db:setup | Initialize database (migrations + setup) |
pnpm db:repair-untitled | Repair "Untitled Ad" entries with YouTube API |
pnpm db:migrate-urls | Migrate proxy URLs to original image URLs |
Testing Scripts
| Script | Description |
|---|---|
pnpm test:video-analysis | Test 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:
- Reads
src/lib/db/schema.ts - Compares with current database state
- Generates SQL diff in
drizzle/directory - Run
pnpm db:pushto apply
pnpm db:push
Applies migrations to database:
- Reads migration files from
drizzle/ - Executes SQL against
DATABASE_URL - 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
- Push to GitHub
git add .
git commit -m "Ready for deployment"
git push origin main
- Import to Vercel
- Go to https://vercel.com/new
- Import your GitHub repository
- Select "Next.js" as framework preset
- 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
- Deploy
- Click "Deploy"
- Wait for build to complete
- Visit your deployment URL
- 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
/tmpdirectory - Cold starts: First request may be slower
Production Database Setup
Supabase Production Database:
- Create production Supabase project
- Copy connection string from Settings → Database
- Add to Vercel environment variables as
DATABASE_URL - 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
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL | Yes | Supabase project URL |
SUPABASE_ANON_KEY | Yes | Supabase anonymous key |
DATABASE_URL | Yes | PostgreSQL connection string |
OPENAI_API_KEY | Yes | OpenAI API key (GPT models) |
GOOGLE_API_KEY | Yes | Google Gemini API key (video analysis) |
NEXT_PUBLIC_SITE_URL | Yes | Public URL of deployment |
APIFY_API_TOKEN | No | Apify token (Google Ads scraping) |
FIRECRAWL_API_KEY | No | Firecrawl API key (landing page scraping) |
YOUTUBE_API_KEY | No | YouTube Data API key (metadata) |
DATAFORSEO_LOGIN | No | DataForSEO login (competitor discovery) |
DATAFORSEO_PASSWORD | No | DataForSEO password |
LOG_LEVEL | No | Logging level (default: info) |
LOG_FORMAT | No | Logging 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:
- Frontend requests
/api/proxy-image?url=https://... - Backend fetches image from Google Ads
- 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:
- Find ads with headline "Untitled Ad"
- Extract video ID from videoUrl
- Fetch title/description from YouTube Data API v3 or youtubei.js
- 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:
- User triggers analysis
- Create task in
analysis_taskstable with status "pending" - Enqueue background job
- Job worker processes analysis
- Update task status to "completed" or "failed"
- 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
- User triggers workflow from UI
- 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)
- Update progress in working memory
- 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 Supabasefeature-ai-video-ad-analysis.md- Video analysis with Google Geminifeature-landing-page-analysis.md- Firecrawl integration for landing page scrapingfeature-bulk-report-generation.md- Batch analysis with Mastra agentsfeature-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 debuggingbug-fix-apify-image-cors-proxy.md- CORS proxy for Google Ads imagesbug-fix-missing-userId-fields.md- User-scoped data isolation fixbug-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 Supabasechore-setup-drizzle-orm.md- Drizzle ORM integrationchore-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 schemarefactor-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 designchore-migrate-libsql-to-supabase.md- Migration to Supabase
AI Analysis
feature-ai-video-ad-analysis.md- Gemini video analysisfeature-bulk-report-generation.md- Mastra agent batch analysisfeature-landing-page-analysis.md- Firecrawl landing page scraping
Scraping
apify-google-ads-scraper-integration.md- Apify integrationautomated-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
| Package | Version | Purpose |
|---|---|---|
next | 15.3.3 | React framework |
react | 19.1.1 | UI library |
typescript | 5.9.2 | Type safety |
drizzle-orm | 0.44.6 | Database ORM |
@supabase/supabase-js | 2.75.0 | Supabase client |
@mastra/core | 0.20.2 | AI agent framework |
@copilotkit/react-core | 1.10.6 | AI canvas integration |
@ai-sdk/openai | 2.0.50 | OpenAI integration |
@google/genai | 1.24.0 | Gemini integration |
apify-client | 2.18.0 | Apify scraping |
youtubei.js | 16.0.1 | YouTube downloads |
tailwindcss | 4.1.12 | CSS framework |
lucide-react | 0.542.0 | Icon library |
zod | 3.25.76 | Schema validation |
pino | 10.0.0 | Logging |
🛠️ Troubleshooting
Common Issues
Authentication Issues
Problem: "Unauthorized" error on dashboard pages
Solutions:
- Check Supabase credentials in
.env:echo $SUPABASE_URL echo $SUPABASE_ANON_KEY - Verify middleware is running (check
middleware.ts) - Clear browser cookies and login again
- Check Supabase project is active (not paused)
- 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:
- Verify
DATABASE_URLis correct:echo $DATABASE_URL - Check Supabase project is active
- Test connection with Drizzle Studio:
pnpm db:studio - Verify IP whitelist in Supabase → Settings → Database
Problem: Migration fails with "relation already exists"
Solutions:
- Drop existing tables in Supabase SQL Editor
- Re-run migration:
pnpm db:push - 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:
- Ensure
youtubei.jsis installed:pnpm install youtubei.js - Check video URL is valid YouTube URL
- Verify
/tmpdirectory has space (512MB limit on Vercel) - Review Vercel function logs for errors
Problem: Gemini API rate limit exceeded
Solutions:
- Implement exponential backoff retry logic
- Switch to Gemini 1.5 Flash (cheaper, faster)
- Upgrade Gemini API quota
- Add delay between video analysis requests
Problem: Video analysis returns "Invalid video format"
Solutions:
- Verify video URL is YouTube URL (not other platforms)
- Check video is public (not private/unlisted)
- Try with
YOUTUBE_COOKIESfor unlisted videos
API Issues
Problem: "API key invalid" errors
Solutions:
- Verify API keys are active:
curl https://api.openai.com/v1/models \ -H "Authorization: Bearer $OPENAI_API_KEY" - Check API keys have sufficient credits
- Verify API keys in
.envmatch Vercel environment variables - Regenerate API keys if expired
Problem: Apify scraping returns 0 ads
Solutions:
- Verify Google Advertiser ID is correct (format:
AR...) - Check advertiser has active ads on selected platforms
- Try different target countries/languages
- Review Apify actor logs in Apify console
Problem: "Rate limit exceeded" from OpenAI/Gemini
Solutions:
- Implement request queuing with delays
- Upgrade API tier for higher rate limits
- Add exponential backoff retry logic
- 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:
- Verify image URL is correct
- Check image URL is accessible externally
- Review proxy endpoint logs
- Ensure
NEXT_PUBLIC_SITE_URLis set correctly
Build Issues
Problem: TypeScript errors during build
Solutions:
- Run type check:
pnpm tsc --noEmit - Fix type errors in reported files
- Ensure all imports are correct
- Restart TypeScript server
Problem: "Module not found" errors
Solutions:
- Verify dependencies are installed:
pnpm install - Check import paths are correct (use
@/for src/) - Restart dev server
- Clear
.nextcache: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
- GitHub Issues: https://github.com/your-username/dominad/issues
- Mastra Docs: https://mastra.ai/docs
- Supabase Docs: https://supabase.com/docs
- Next.js Docs: https://nextjs.org/docs
- CopilotKit Docs: https://docs.copilotkit.ai
📄 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
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m "Add my feature" - Push to the branch:
git push origin feature/my-feature - 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