Architecture Deep Dive
A detailed walkthrough of the runtime, data model, request flows, Stripe integration, privacy model, and operational considerations for AdSwap.
Runtime & Hosting
- Next.js (Pages Router) on Vercel — unified front-end and API routes.
- API routes of note:
/api/create-checkout-session
,/api/stripe-webhook
. - Supabase (Postgres, Auth) for users, profiles and future pass/accounting tables.
- Stripe Checkout for funding/passes; Stripe webhooks for authoritative state.
- Payment toggle:
ENABLE_PAYMENTS
andNEXT_PUBLIC_ENABLE_PAYMENTS
switch between demo and real mode.
Data Model
Current tables and proposed extensions:
profiles ( id uuid primary key references auth.users(id), is_premium boolean not null default false, stripe_customer_id text unique ) -- Proposed for passes & accounting (not required for demo) passes ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, domain text not null, expires_at timestamptz not null, amount_cents int not null default 5, status text not null default 'active' ) transactions ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, stripe_event_id text, amount_cents int not null, kind text not null -- 'charge' | 'refund' | 'adjustment' )
Domains should be stored hashed+salted; only prefixes for matching if needed.
Primary Flows
- Top‑up: User funds balance via Stripe Checkout; webhook writes
transactions
. - Chip → Pass: Clicking chip requests a pass for domain. In demo, the API grants immediately; in prod it verifies balance/creates a charge.
- Decision: Server returns a signed decision (domain, expiry). Client hides ad slots; extension stores expiry locally.
- Payouts: Stripe events accumulate per publisher; periodic job aggregates into payout reports.
POST /api/create-checkout-session { userId, domain? } if ENABLE_PAYMENTS=false => grant pass directly else => stripe.checkout.sessions.create(...) Webhook /api/stripe-webhook checkout.session.completed => record transaction, optionally grant initial pass invoice.payment_succeeded => renew subscriptions/credits customer.subscription.* => sync entitlement state
Payments & Webhooks
- Stripe Checkout for one‑off funding or subscriptions (credits).
- Webhooks:
checkout.session.completed
,invoice.payment_succeeded
,customer.subscription.deleted
. - All updates occur server‑side via the Supabase admin client using the service role key.
- Demo mode short‑circuits Stripe and marks profiles/passes directly.
Privacy & Security
- No URL storage; only domains (normalized), optionally hashed with per‑env salts.
- No cross‑site tracking; decisions are domain‑scoped and time‑boxed.
- Signed decisions (JWT or HMAC) to prevent tampering; extension validates before hiding slots.
- Publishers receive only aggregated accruals; no reader identity is shared.
- Rate limiting on API routes (IP + user) to prevent abuse; idempotent webhook handlers.
Operations: Environments & Observability
- Environments via
.env(.local)
and Vercel envs; secrets never exposed client‑side. - Structured logs in API routes; webhook logs include event IDs to ensure exactly‑once behavior.
- Health: simple
/api/hello
plus Stripe webhook signature checks. - Backfill scripts: SQL in
supabase/
for schema, future migrations forpasses
/transactions
.
Publisher Integration
A minimal script to render the chip and receive decisions.
<script> (async function(){ const domain = location.hostname.replace(/^www./,''); // render chip (or use the extension if installed) // call your backend to request a decision; respect expiry })(); </script>
The extension supersedes the script when present to avoid duplication.
Extension Behavior
- Renders a chip only on partner domains (or optional advanced mode).
- On confirm: requests a signed pass; stores expiry; triggers a gentle reload to avoid flicker.
- Observes the DOM for ad leaks; extends pass time or triggers small refunds when detected.
- Popup shows active passes with countdown, top‑up shortcuts, and receipts.