Skip to content

kevincui1034/adflow

Repository files navigation

AdFlow — System Design Overview

AdFlow is a TikTok-style ad-discovery and ad-generation platform. It scrapes trending short-form video content (TikTok, YouTube Shorts), lets brands remix those trends into AI-generated ads (copy + image), and publishes finished ads back into a swipeable public feed.

This document describes the end-to-end system architecture so you can work against the backend from a local CLI or any other client.


1. High-level architecture

┌────────────────────┐        ┌──────────────────────────────────────────┐
│  React SPA (Vite)  │        │              Supabase project            │
│  — /feed (public)  │ ─────▶ │  Postgres + RLS                          │
│  — /trends         │        │  Auth (email + Google)                   │
│  — /ads (gen)      │        │  Storage buckets: avatars, ad-images     │
│  — /my-ads         │        │  Edge Functions (Deno):                  │
└─────────┬──────────┘        │   • ad-chat        (streaming SSE)       │
          │                   │   • ad-generate    (brief + image)       │
          │  HTTPS + JWT      │   • scrape-trends  (Apify ingestion)     │
          ▼                   │   • industry-news  (NewsAPI / mock)      │
   supabase-js client         └─────────────┬────────────────────────────┘
          │                                 │
          ▼                                 ▼
      PostHog                ┌──────────────┼──────────────┬─────────────┐
   (product analytics)       ▼              ▼              ▼             ▼
                      Google Gemini API  Apify Actors  NewsAPI.org   Firecrawl
                 (generativelanguage  (TikTok, YouTube,  (industry    (homepage
                    .googleapis.com)   Instagram)       headlines)    scraping)
                 gemini-2.5-flash (text + tool calls)
                 gemini-2.5-flash-image (ad visuals)

Key idea: the frontend never talks directly to third parties. All AI / scraping / news calls go through Supabase Edge Functions, which hold the secrets. PostHog is the one exception — the browser sends events directly using the publishable VITE_POSTHOG_KEY.


2. Backend project (Supabase)

Item Value
Project ref from VITE_SUPABASE_PROJECT_ID
API base URL from VITE_SUPABASE_URL (e.g. https://<ref>.supabase.co)
Edge Functions base ${VITE_SUPABASE_URL}/functions/v1
Publishable (anon) key from VITE_SUPABASE_PUBLISHABLE_KEY (safe in code)

For a CLI you only need the API base URL + anon key. For privileged operations (e.g. seeding data) you'll need a service role key — fetch it from the Supabase Dashboard → Project Settings → API.


3. Database schema

All tables live in the public schema. Row-Level Security is enabled on every table.

profiles

One row per auth user. Created automatically by the handle_new_user() trigger.

  • id uuid (= auth.users.id)
  • company_name, company_email, industry (enum), avatar_url
  • website_url text — derived from the company email domain during onboarding
  • company_context jsonb — auto-enriched brand data (see §6 Onboarding enrichment)
  • onboarding_complete bool
  • RLS: user can SELECT/INSERT/UPDATE only their own row.

user_roles

Roles stored separately to prevent privilege-escalation bugs.

  • (user_id, role) where role is enum app_role = 'admin' | 'user'
  • Read via has_role(_user_id, _role) security-definer SQL function.
  • RLS: users can read their own roles; only the trigger inserts.

trends

Scraped short-form videos from TikTok / YouTube Shorts.

  • external_id text, platform text ('TikTok' or 'YouTube Shorts')
  • industry industry_type enum (technology, consumer_products, gas_energy, b2b_saas, healthcare, finance, retail, media, education, other)
  • Metrics: views, likes, comments, shares (bigint), engagement_rate numeric
  • Media: title, description, thumbnail_url, video_url, creator, hashtags text[]
  • raw_payload jsonb — full upstream object (used to build deep-link fallback URLs)
  • RLS: anyone (incl. anon) can SELECT; only admin role can mutate.

generated_ads

AI-generated ads owned by users. Some are flipped public so they appear on the landing feed.

  • user_id uuid, headline, body, cta, image_prompt text
  • image_url text (CDN URL from the AI image gateway)
  • brief jsonb (full structured output of ad-generate)
  • is_public bool (default false), like_count int
  • RLS: owner can CRUD; anyone can SELECT rows where is_public = true.

chat_messages

Per-session chat history for the ad-briefing assistant.

  • (session_id, user_id, role, content)
  • RLS: owner-only.

Storage buckets

  • avatars (public) — profile pictures.
  • ad-images (public) — reserved for storing AI images permanently. Today generated_ads.image_url points at the gateway CDN.

4. Edge Functions

All functions live under supabase/functions/<name>/index.ts and are auto-deployed. They share a CORS preamble and read secrets from Deno.env.

4.1 ad-chat — streaming chat

  • POST /functions/v1/ad-chat
  • Body: { messages: ChatMsg[], context: { company, industry, trend? } }
  • Calls the Google Gemini REST API (gemini-2.5-flash) through _shared/gemini.ts, which streams :streamGenerateContent?alt=sse and re-emits it as OpenAI-compatible SSE chunks (data: {choices: [{delta: {content: "..."}}]}).
  • Returns text/event-stream. Parse data: {...} lines and pull choices[0].delta.content.
  • Error codes: 429 rate-limit, 402 quota exhausted.

4.2 ad-generate — structured brief + image

  • POST /functions/v1/ad-generate
  • Body: same shape as ad-chat.
  • Two-step pipeline:
    1. Tool-call against gemini-2.5-flash (via the shared adapter) to extract a strict JSON brief: { headline, body, cta, image_prompt, style }.
    2. Image generation via gemini-2.5-flash-image-preview using image_prompt. Returns a base64 data: URL.
  • Returns { brief, image_url }. image_url may be null if image gen fails (brief still returned).

4.3 scrape-trends — Apify ingestion

  • POST /functions/v1/scrape-trends
  • Body (all optional): { industries?: string[], platforms?: ("tiktok"|"youtube")[] }
  • For each industry, uses curated hashtag/keyword seeds (see SEEDS map in the function) to query:
    • clockworks/tiktok-scraper → up to ~10 TikTok videos / industry
    • streamers/youtube-scraper → up to ~12 YouTube Shorts / industry
  • Normalizes both shapes into the trends row format, computes engagement_rate = (likes+comments+shares)/views * 100, dedupes in-batch by (platform, external_id), then upserts into trends.
  • Idempotent — safe to run on a cron.

4.4 industry-news — headlines

  • POST /functions/v1/industry-news with { industry: string }.
  • If NEWS_API_KEY is set → live results from NewsAPI.org filtered by industry query.
  • Otherwise falls back to a curated mock list per industry. Used by the dashboard for "what's happening in your space."

4.5 enrich-company — onboarding brand enrichment

  • POST /functions/v1/enrich-company
  • Body: { company_name, company_email, industry }
  • Pipeline (all best-effort, never blocks signup):
    1. Domain derived from email (skips free providers like gmail/yahoo).
    2. Logo auto-fetched from Clearbit Logo API (https://logo.clearbit.com/<domain>) — no key needed.
    3. Website scrape via Firecrawl (if FIRECRAWL_API_KEY is set) — markdown of the homepage.
    4. Brand inference via Google Gemini (gemini-2.5-flash through _shared/gemini.ts) using a strict tool-call schema → returns tagline, description, brand tone, product categories, target audience (persona / age / interests), 3–5 competitors, 5–10 hashtags.
    5. Company news via NewsAPI scoped to the company name (5 most recent articles).
  • Persists everything to profiles.company_context jsonb and sets profiles.website_url.
  • Fires scrape-trends for the user's industry in the background to pre-warm the feed.

5. Secrets (server-side only)

Stored as Supabase project secrets (supabase secrets set KEY=value), injected into edge functions as env vars. Never shipped to the browser.

Secret Used by Purpose
GEMINI_API_KEY ad-chat, ad-generate, enrich-company, competitor-analysis Google AI Studio key for Gemini REST API
APIFY_TOKEN scrape-trends, social-trends Run Apify actors
NEWS_API_KEY industry-news, enrich-company, competitor-analysis Live headlines from NewsAPI.org
FIRECRAWL_API_KEY enrich-company, competitor-analysis (optional) Scrape company / competitor homepages
SUPABASE_* All functions Auto-injected by the platform

6. Auth & RLS model

  • Provider: email/password + Google OAuth (native Supabase Auth).
  • On signup, the handle_new_user() trigger inserts a profiles row and a user_roles row with role 'user'.
  • Frontend uses supabase.auth for session; the JWT is automatically attached to PostgREST + Edge Function calls.
  • Public landing feed (/feed) reads anonymously thanks to:
    • trends: Anyone can view trends policy
    • generated_ads: Anyone can view public ads policy (is_public = true)

7. Frontend → backend touchpoints

UI Backend call
/feed from('generated_ads').select(...).eq('is_public',true) + from('trends').select(...)
/trends from('trends').select(...) filtered by user industry
/ads chat panel fetch(/functions/v1/ad-chat) — SSE stream
/ads Generate button supabase.functions.invoke('ad-generate', { body })
/ads Save / Publish from('generated_ads').insert({ ..., is_public })
/my-ads from('generated_ads').select(...).eq('user_id', uid)
Onboarding from('profiles').upsert(...)
Admin / cron supabase.functions.invoke('scrape-trends')

8. Hitting the backend from a local CLI

Minimal Node example using @supabase/supabase-js:

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.VITE_SUPABASE_URL!,
  process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,
);

// Sign in (or use service role key for admin scripts)
await supabase.auth.signInWithPassword({ email, password });

// Read public feed
const { data: ads } = await supabase
  .from("generated_ads")
  .select("id, headline, body, cta, image_url, like_count")
  .eq("is_public", true)
  .order("created_at", { ascending: false });

// Trigger a trend scrape
const { data, error } = await supabase.functions.invoke("scrape-trends", {
  body: { industries: ["technology"], platforms: ["tiktok", "youtube"] },
});

// Generate an ad
const gen = await supabase.functions.invoke("ad-generate", {
  body: {
    messages: [{ role: "user", content: "Promote our new air fryer to Gen Z" }],
    context: { company: "CrispCo", industry: "consumer_products" },
  },
});
console.log(gen.data); // { brief: {...}, image_url: "https://..." }

For raw HTTP, every edge function expects:

POST ${VITE_SUPABASE_URL}/functions/v1/<name>
Authorization: Bearer <anon-or-user-jwt>
Content-Type: application/json

The streaming ad-chat endpoint returns text/event-stream; consume with fetch().body.getReader(), split on \n, then JSON-parse each data: ... line.


9. Local dev for the web app

npm install
cp .env.example .env      # fill in your own keys
npm run dev               # Vite at http://localhost:8080
npm test                  # Vitest

Edge-function local dev

Edge functions read secrets from supabase/functions/.env when served locally:

# one-off: wire up secrets for local serve
cat > supabase/functions/.env <<EOF
GEMINI_API_KEY=...
APIFY_TOKEN=...
NEWS_API_KEY=...
FIRECRAWL_API_KEY=...
SUPABASE_URL=${VITE_SUPABASE_URL}
SUPABASE_SERVICE_ROLE_KEY=...
EOF

supabase functions serve

For production, set the same keys as project secrets:

supabase secrets set GEMINI_API_KEY=... APIFY_TOKEN=... NEWS_API_KEY=... FIRECRAWL_API_KEY=...
supabase functions deploy ad-chat ad-generate enrich-company competitor-analysis scrape-trends social-trends industry-news

The Supabase client (src/integrations/supabase/client.ts) is thin — it reads VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY from import.meta.env. Types (src/integrations/supabase/types.ts) are regenerated from the database schema with supabase gen types typescript.

Analytics (PostHog)

The browser initializes PostHog in src/lib/posthog.ts using VITE_POSTHOG_KEY / VITE_POSTHOG_HOST. AuthContext automatically calls posthog.identify(userId, { email }) on sign-in and posthog.reset() on sign-out. Turn it off locally by leaving VITE_POSTHOG_KEY blank.


10. Operational notes

  • Trend freshness: scrape-trends is on-demand today. Schedule it via a cron (Supabase scheduled function or external scheduler) to keep the feed lively.
  • AI cost control: Both ad-chat and ad-generate propagate 429 (rate limit) and 402 (credits exhausted) so the UI can show actionable toasts.
  • Image storage: Generated images are currently referenced by their gateway CDN URL. If you need permanent storage, download the image in ad-generate and upload to the ad-images bucket before returning image_url.
  • RLS first: any new table must enable RLS and define explicit policies — otherwise nothing will be readable through the anon key.

About

Adflow, your marketing agent to go viral!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages