Skip to main content

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

Build docs developers (and LLMs) love