Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/MatthewSabia1/AdRecon/llms.txt

Use this file to discover all available pages before exploring further.

Overview

AdRecon V2 is a modern SaaS platform built with a Vercel-hosted static site frontend and Supabase-powered backend. The architecture prioritizes:
  • Direct frontend-to-database queries with row-level security (RLS)
  • Serverless API functions for media proxying, admin operations, and page capture
  • Single-page application (SPA) routing with authentication gating
  • Production-grade deployment workflow with staging and production branches
For complete implementation details, see docs/agent-reference.md in the source repository.

Stack Components

Frontend

  • Vite + React + TypeScript
  • Tailwind CSS for styling
  • Framer Motion for animations
  • Lucide React icons
  • Sonner for toast notifications

Backend

  • Supabase Postgres (views + tables with RLS)
  • Supabase Auth (Magic Link + Google OAuth)
  • Vercel Serverless Functions (Node.js)

Hosting

  • Vercel static site hosting
  • Vercel Edge Network for global distribution
  • SPA rewrites via vercel.json

Capture Pipeline

  • Puppeteer Core + @sparticuz/chromium
  • SingleFile for HTML snapshots
  • Archiver for ZIP packaging

Runtime Topology

Hosting Model

AdRecon is deployed as a Vercel static site built from the dist/ directory:
  • App Shell: app/index.html is served at both / and /app via Vercel rewrites
  • Build Output: Mirrors the SPA shell at dist/index.html and dist/app/index.html
  • Vite Entry Shim: app/src/main.tsx imports src/main.tsx to ensure /app works in both dev and production
  • Base Path: Vite uses / in dev and /app/ in production so assets resolve correctly

Route Architecture

/              → /app/index.html (SPA auth shell)
/index.html    → /app/index.html

SPA Routing Logic

The authentication gate in src/App.tsx determines which component renders:
1

Session Check

Supabase client (initialized in src/lib/supabase.ts) checks for an active session.
2

Unauthenticated State

If no session exists, render the <Auth> component (Magic Link + Google OAuth).
3

Authenticated Routing

For authenticated users, route to the appropriate view:
  • /admin or /app/admin: Render <AdminDashboard> if user.raw_app_meta_data.user_type === 'admin', otherwise show access warning
  • /admin/fanbasis or /app/admin/fanbasis: Render <FanbasisAdmin> (admin-only)
  • /profile or /app/profile: Render <ProfilePage>
  • All other routes: Render <Dashboard> (main ad feed interface)

Data Flow

Feed Query Architecture

AdRecon queries Supabase directly from the frontend using three feed scopes:
Data Source: public.ads_feed_v2
// src/lib/ads.ts
const { data, error } = await supabase
  .from('ads_feed_v2')
  .select('*')
  .order('created_at', { ascending: false })
  .range(offset, offset + limit - 1);
This normalized view aggregates raw data from public.apify_ads_raw with:
  • Fixed network classification (ClickBank, Digistore24, BuyGoods, MaxWeb, Other)
  • Fixed taxonomy classification (Health, Wealth, BizOpp, Relationship, Survival, Other)
  • High-quality media URL prioritization

Saved Ads Persistence

1

Save Action

User clicks Save in the Ad Modal.
2

Database Insert

Frontend calls saveAd(userId, adArchiveId) from src/lib/savedAds.ts:
await supabase
  .from('user_saved_ads')
  .insert({ user_id: userId, ad_archive_id: adArchiveId });
Composite key (user_id, ad_archive_id) prevents duplicates.
3

RLS Enforcement

Supabase RLS policy ensures auth.uid() = user_id, isolating saved ads per user.

Project Management

1

Create Project

User creates a project via the Dashboard:
// src/lib/projects.ts
await supabase
  .from('user_projects')
  .insert({ user_id: userId, name: projectName });
2

Assign Ad to Project

User assigns a saved ad to one or more projects:
await supabase
  .from('user_saved_ad_projects')
  .insert({ user_id: userId, ad_archive_id: adId, project_id: projectId });
3

Query Project Feed

Dashboard queries user_project_ads_feed_v1 filtered by project_id.

Serverless API Functions

Media Proxy (/api/ad-media)

Purpose: Secure image proxy for ad creatives to avoid CORS and mixed-content issues. Implementation:
  • Vercel serverless function (api/ad-media.js)
  • Fetches external image URLs and returns the binary response
  • Passes through Content-Type headers
  • No authentication required (public endpoint)
Usage:
<img src="/api/ad-media?url=https://example.com/creative.jpg" />

Admin User Management (/api/admin/users)

Purpose: Admin-only endpoint for user CRUD operations. Implementation:
  • Vercel serverless function (api/admin/users.js)
  • Requires SUPABASE_SERVICE_ROLE_KEY for elevated database access
  • Validates admin status via auth.users.raw_app_meta_data.user_type
  • Supports creating, updating, and deleting users
This endpoint bypasses RLS and requires service role credentials. Admin status must be verified on every request.

Fanbasis Integration (/api/admin/fanbasis)

Purpose: Admin controls for Fanbasis product sync and webhook registration. Implementation:
  • Vercel serverless function
  • Requires FANBASIS_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
  • Admin-gated actions for:
    • API health check
    • Product list sync
    • Webhook registration
    • Offer enable/disable toggles

Fanbasis Webhook (/api/fanbasis/webhook)

Purpose: Receive and log Fanbasis payment events. Implementation:
  • Validates webhook signature using secret from public.integration_secrets
  • Logs events to public.fanbasis_webhook_log
  • Handles refund/dispute events by revoking user access

Page Ripper (/api/download-page)

Purpose: Inline landing page capture using headless Chromium. Implementation (api/download-page.js):
1

Request Validation

  • Authenticate user via Supabase session
  • Validate rate limit: 10 captures per user per 15 minutes (logged in page_rip_log)
  • Check SSRF safety: hostname blocklist + DNS verification
2

Headless Capture

  • Launch Puppeteer with @sparticuz/chromium
  • Navigate to target URL with 110-second AbortController timeout
  • Auto-scroll to trigger lazy-loaded content
  • Capture HTML using SingleFile
  • Intercept network resources (CSS, JS, images, fonts, media)
3

Resource Packaging

  • Package page.html + assets/ into a ZIP archive using archiver
  • Enforce caps: 100 MB total, 500 resources max
4

Response

  • Return ZIP binary directly in HTTP response
  • No storage bucket, no job queue — entire lifecycle in one request
Page Ripper runs on Vercel serverless infrastructure with a hard 120-second timeout. Captures typically complete in 15-45 seconds.

Database Schema

Core Tables and Views

Type: TableRaw ad data ingested from Apify scrapers. Source table for ads_feed_v2.RLS: Enabled, authenticated role has read access.
Type: ViewNormalized ad feed with:
  • Fixed network classification
  • Fixed taxonomy classification
  • High-quality media URL prioritization
  • Consistent schema for UI consumption
Queried by: Dashboard (all scope)
Type: TablePer-user saved ads with composite key (user_id, ad_archive_id).RLS: auth.uid() = user_id for strict user isolation.Used by: Save/unsave operations (src/lib/savedAds.ts)
Type: TableProject folders owned by users.RLS: auth.uid() = user_idUsed by: Project CRUD operations (src/lib/projects.ts)
Type: TableMany-to-many links between saved ads and projects.RLS: auth.uid() = user_idUsed by: Project assignment logic
Type: ViewJoins user_saved_ads + ads_feed_v2 for saved-only feed.Queried by: Dashboard (saved scope)
Type: ViewJoins user_saved_ad_projectsuser_saved_adsads_feed_v2 for project-scoped feed.Queried by: Dashboard (project scope)
Type: TableRate-limiting log for Page Ripper captures.Access: Service role only (bypasses RLS)Used by: /api/download-page for 10 captures / 15 min enforcement
Type: TableLogs all Fanbasis webhook events for audit trail.Used by: /api/fanbasis/webhook
Type: TableEncrypted storage for third-party integration secrets (e.g., Fanbasis webhook secret).Access: Service role only

Migrations

All schema changes are managed via Supabase migrations in supabase/migrations/:
  • 20260223083000_create_ads_feed_v2_and_saved_ads.sql — Core feed and saved ads
  • 20260224123000_projects_save_system_v3.sql — Projects and project-ad links
  • 20260224190000_prioritize_high_quality_media_urls.sql — Media URL prioritization
  • 20260225010000_add_app_settings_table.sql — App settings persistence
  • 20260225020000_add_fanbasis_tables.sql — Fanbasis integration tables
  • 20260226052000_add_integration_secrets_table.sql — Encrypted secrets storage
  • 20260227_create_page_rip_log.sql — Page Ripper rate limiting
  • 20260223091000_harden_legacy_security_objects.sql — Optional hardening (drops legacy tables)

Security Model

Row-Level Security (RLS)

All user-scoped tables enforce RLS policies:
CREATE POLICY "Users can manage own saved ads"
ON public.user_saved_ads
FOR ALL
USING (auth.uid() = user_id);

Authentication

  • Provider: Supabase Auth
  • Methods: Magic Link (email) + Google OAuth
  • Session Storage: localStorage (Supabase client default)
  • Redirect URLs: Configured via VITE_MAGIC_LINK_REDIRECT_URL and Supabase Dashboard

Admin Authorization

Admin status is stored in auth.users.raw_app_meta_data.user_type:
// src/App.tsx
const isAdmin = user?.user_metadata?.user_type === 'admin';
Admin-only routes and API endpoints check this field before rendering or processing requests.

SSRF Protection (Page Ripper)

The /api/download-page endpoint implements multi-layer SSRF protection:
1

Hostname Blocklist

Reject private/internal IP ranges, localhost, and metadata endpoints.
2

DNS Resolution Check

Resolve hostname to IP and re-validate against the blocklist.
3

Post-Redirect Verification

After Puppeteer navigation, verify the final URL didn’t redirect to a blocked host.

Build and Deployment

Build Process

npm run build executes scripts/build-site.mjs:
1

Clean Output

Remove and recreate dist/ directory.
2

Build SPA

Run npm run build:app (Vite build) → outputs to dist/app/.
3

Duplicate Shell

Copy dist/app/index.html to dist/index.html so / and /app share the same SPA entry.
4

Copy Favicon

Copy favicon.ico if present.

Deployment Workflow

AdRecon uses a strict 2-lane deployment model:
npm run deploy:staging
  • Enforces staging branch checkout
  • Deploys to Vercel Preview environment
  • Script: ./scripts/deploy-vercel.sh staging
The deploy script blocks deployments from the wrong branch to prevent accidental production releases from staging.

Vercel Configuration

vercel.json defines:
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "rewrites": [
    { "source": "/", "destination": "/app/index.html" },
    { "source": "/index.html", "destination": "/app/index.html" },
    { "source": "/admin", "destination": "/app/index.html" },
    { "source": "/admin/:path*", "destination": "/app/index.html" },
    { "source": "/profile", "destination": "/app/index.html" },
    { "source": "/profile/:path*", "destination": "/app/index.html" },
    { "source": "/app", "destination": "/app/index.html" },
    { "source": "/app/:path*", "destination": "/app/index.html" }
  ]
}

Environment Variables

Frontend Variables

VITE_SUPABASE_URL
string
required
Supabase project URL for frontend client initialization
VITE_SUPABASE_ANON_KEY
string
required
Supabase anonymous (public) key for RLS-enforced queries
Override for magic link auth redirect (defaults to <origin>/app)

Backend Variables

SUPABASE_URL
string
required
Supabase URL for serverless functions (admin operations)
SUPABASE_SERVICE_ROLE_KEY
string
required
Service role key for RLS-bypassing admin operations
FANBASIS_API_KEY
string
API key for Fanbasis integration actions
Server-side redirect override for Fanbasis purchase magic links
ADMIN_RESET_REDIRECT_URL
string
Password reset email redirect target for admin-created users
APIFY_TOKEN
string
Apify API token for Landing Ripper actor runs (legacy, unused by current Page Ripper)
APIFY_LANDING_RIPPER_ACTOR_ID
string
Apify actor ID (legacy, unused by current Page Ripper)
APIFY_LANDING_RIPPER_WEBHOOK_SECRET
string
Webhook validation secret (legacy, unused by current Page Ripper)
APP_BASE_URL
string
Absolute public app URL for building webhook/download links

System Diagram

Performance Considerations

Direct Database Queries

Frontend queries Supabase directly (no API middleware) for minimal latency. RLS enforcement happens at the database layer.

Media Proxy Caching

/api/ad-media can be extended with CDN caching headers to reduce external fetch frequency.

Feed Pagination

Dashboard implements offset-based pagination with configurable page size to manage large result sets.

Serverless Cold Starts

Page Ripper and admin endpoints may experience cold starts (1-3 seconds). Consider warming functions for critical paths.

Next Steps

Back to Introduction

Return to the platform overview

Quickstart Guide

Learn how to use AdRecon as an end user