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 thedist/ directory:
- App Shell:
app/index.htmlis served at both/and/appvia Vercel rewrites - Build Output: Mirrors the SPA shell at
dist/index.htmlanddist/app/index.html - Vite Entry Shim:
app/src/main.tsximportssrc/main.tsxto ensure/appworks in both dev and production - Base Path: Vite uses
/in dev and/app/in production so assets resolve correctly
Route Architecture
- Public Routes
- Authenticated Routes
- API Routes
- Static Assets
SPA Routing Logic
The authentication gate insrc/App.tsx determines which component renders:
Unauthenticated State
If no session exists, render the
<Auth> component (Magic Link + Google OAuth).Authenticated Routing
For authenticated users, route to the appropriate view:
/adminor/app/admin: Render<AdminDashboard>ifuser.raw_app_meta_data.user_type === 'admin', otherwise show access warning/admin/fanbasisor/app/admin/fanbasis: Render<FanbasisAdmin>(admin-only)/profileor/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:- All Ads (Default)
- Saved Ads
- Project Ads
Data Source: This normalized view aggregates raw data from
public.ads_feed_v2public.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
Database Insert
Frontend calls Composite key
saveAd(userId, adArchiveId) from src/lib/savedAds.ts:(user_id, ad_archive_id) prevents duplicates.Project Management
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-Typeheaders - No authentication required (public endpoint)
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_KEYfor elevated database access - Validates admin status via
auth.users.raw_app_meta_data.user_type - Supports creating, updating, and deleting users
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):
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
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)
Resource Packaging
- Package
page.html+assets/into a ZIP archive usingarchiver - Enforce caps: 100 MB total, 500 resources max
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
public.apify_ads_raw
public.apify_ads_raw
Type: TableRaw ad data ingested from Apify scrapers. Source table for
ads_feed_v2.RLS: Enabled, authenticated role has read access.public.ads_feed_v2
public.ads_feed_v2
Type: ViewNormalized ad feed with:
- Fixed network classification
- Fixed taxonomy classification
- High-quality media URL prioritization
- Consistent schema for UI consumption
all scope)public.user_saved_ads
public.user_saved_ads
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)public.user_projects
public.user_projects
Type: TableProject folders owned by users.RLS:
auth.uid() = user_idUsed by: Project CRUD operations (src/lib/projects.ts)public.user_saved_ad_projects
public.user_saved_ad_projects
Type: TableMany-to-many links between saved ads and projects.RLS:
auth.uid() = user_idUsed by: Project assignment logicpublic.user_saved_ads_feed_v1
public.user_saved_ads_feed_v1
Type: ViewJoins
user_saved_ads + ads_feed_v2 for saved-only feed.Queried by: Dashboard (saved scope)public.user_project_ads_feed_v1
public.user_project_ads_feed_v1
Type: ViewJoins
user_saved_ad_projects → user_saved_ads → ads_feed_v2 for project-scoped feed.Queried by: Dashboard (project scope)public.page_rip_log
public.page_rip_log
Type: TableRate-limiting log for Page Ripper captures.Access: Service role only (bypasses RLS)Used by:
/api/download-page for 10 captures / 15 min enforcementpublic.fanbasis_webhook_log
public.fanbasis_webhook_log
Type: TableLogs all Fanbasis webhook events for audit trail.Used by:
/api/fanbasis/webhookpublic.integration_secrets
public.integration_secrets
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 insupabase/migrations/:
20260223083000_create_ads_feed_v2_and_saved_ads.sql— Core feed and saved ads20260224123000_projects_save_system_v3.sql— Projects and project-ad links20260224190000_prioritize_high_quality_media_urls.sql— Media URL prioritization20260225010000_add_app_settings_table.sql— App settings persistence20260225020000_add_fanbasis_tables.sql— Fanbasis integration tables20260226052000_add_integration_secrets_table.sql— Encrypted secrets storage20260227_create_page_rip_log.sql— Page Ripper rate limiting20260223091000_harden_legacy_security_objects.sql— Optional hardening (drops legacy tables)
Security Model
Row-Level Security (RLS)
All user-scoped tables enforce RLS policies:- user_saved_ads
- user_projects
- user_saved_ad_projects
Authentication
- Provider: Supabase Auth
- Methods: Magic Link (email) + Google OAuth
- Session Storage:
localStorage(Supabase client default) - Redirect URLs: Configured via
VITE_MAGIC_LINK_REDIRECT_URLand Supabase Dashboard
Admin Authorization
Admin status is stored inauth.users.raw_app_meta_data.user_type:
SSRF Protection (Page Ripper)
The/api/download-page endpoint implements multi-layer SSRF protection:
Build and Deployment
Build Process
npm run build executes scripts/build-site.mjs:
Deployment Workflow
AdRecon uses a strict 2-lane deployment model:- Staging
- Production
- Enforces
stagingbranch checkout - Deploys to Vercel Preview environment
- Script:
./scripts/deploy-vercel.sh staging
Vercel Configuration
vercel.json defines:
Environment Variables
Frontend Variables
Supabase project URL for frontend client initialization
Supabase anonymous (public) key for RLS-enforced queries
Override for magic link auth redirect (defaults to
<origin>/app)Backend Variables
Supabase URL for serverless functions (admin operations)
Service role key for RLS-bypassing admin operations
API key for Fanbasis integration actions
Server-side redirect override for Fanbasis purchase magic links
Password reset email redirect target for admin-created users
Apify API token for Landing Ripper actor runs (legacy, unused by current Page Ripper)
Apify actor ID (legacy, unused by current Page Ripper)
Webhook validation secret (legacy, unused by current Page Ripper)
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