A multi-tenant campaign and payouts platform for short-form creators. Built with Next.js App Router, Supabase (Postgres + Auth + Edge Functions), Shadcn UI, Tailwind, and Stripe. Includes a metrics ingestion pipeline using Bright Data and Supabase Edge Functions.
- Admins, Content Managers, and Creators collaborate in organizations (tenants)
- Managers define campaign payout rules; Admins approve them
- Creators upload creatives with multiple URLs (TikTok, Instagram, YouTube, Facebook)
- Metrics are periodically scraped and aggregated for payouts
- Statements are generated and payouts executed (test mode) via Stripe
- Features
- Architecture
- Getting Started
- Database & RBAC
- Stripe Integration
- Metrics Pipeline
- Project Structure
- Role Guides
- Development
- Deployment
- Troubleshooting
- Works across the Next.js stack (App Router, Middleware, Server Actions, Route Handlers)
- Supabase SSR client using cookies (server and client helpers)
- Multi-tenant RBAC:
tenant_admin,manager,creatorplus platform admins - Campaign rules with approval gates
- Creatives with multiple platform URLs and URL invalidation
- Metrics ingestion (views, likes, comments) and timeseries storage
- Payout previews and statement generation; Stripe Connect onboarding and webhook sync
- UI built with Shadcn UI, Radix UI, Tailwind, and TanStack Table
- Frontend: Next.js 14 (App Router), TypeScript (strict), Shadcn UI, Tailwind
- Auth: Supabase Auth (SSR cookies via
@supabase/ssr) - Data: Supabase Postgres with strict RLS and RPCs encapsulating logic
- Payments: Stripe (Connect Express for creators) in test mode
- Metrics: Supabase Edge Functions (Deno) for scheduler/worker and Bright Data client
Key helpers:
lib/supabase/server.ts– server-side Supabase client and active tenant helpers (requireActiveTenant,getActiveTenantIdOrNull)lib/supabase/client.ts– browser Supabase clientlib/supabase/middleware.ts– auth cookie refresh and guarded routing, wired viamiddleware.tsapp/api/**– server endpoints (e.g., Stripewebhook,onboard,login-link)sql/rbac.sql– database schema, RLS, triggers, and RPCs (includes files undersql/rbac/)
- Node 18+ and npm
- Supabase project (cloud or local)
- Stripe account (test mode)
- Bright Data account for metrics datasets
Create .env.local and set:
# Supabase (client)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=your_supabase_anon_key
# Supabase (server/admin)
SUPABASE_SERVICE_KEY=your_service_role_key
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# App URL (used for Stripe return/login links)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# or when on Vercel
# NEXT_PUBLIC_VERCEL_URL=your-app.vercel.app- Install dependencies
npm install- Prepare Supabase schema
- Ensure your Supabase database has the schema and RPCs from
sql/rbac.sqland the split files undersql/rbac/. - You can run
sql/rbac.sqlend-to-end in the Supabase SQL editor or viapsql. - See
sql/README.mdfor a compact overview and execution order.
- Start the dev server
npm run devApp runs at http://localhost:3000.
- (Optional) Start Stripe CLI webhook forwarding for local testing
stripe listen --forward-to localhost:3000/api/stripe/webhookSee sql/README.md and sql/rbac.sql for full details. Highlights:
- Roles:
tenant_admin > manager > creatorwith union-of-roles per user per tenant - Platform admin: separate table granting global operator capabilities
- Core tables: tenants, tenant_memberships, campaigns, campaign_creators, creatives, creative_urls, creative_url_metrics, payout_accounts, payout_statements, payout_line_items
- RLS everywhere; helper functions enforce permissions (
me_has_tenant_permission,is_my_creative, etc.) - RPCs for workflows:
create_tenant,create_campaign,approve_campaign,campaign_invite_creator,creative_upload,creative_add_url,creative_invalidate_url,preview_creator_payout,create_payout_statement_from_preview,mark_payout_status, etc.
Tenant resolution pattern (server):
import { requireActiveTenant } from "@/lib/supabase/server";
const tenant = await requireActiveTenant();Always scope queries with the active tenant on the server.
Endpoints:
POST /api/stripe/onboard– creates or retrieves a Stripe Connect Express account, returns onboarding linkPOST /api/stripe/login-link– returns a Stripe dashboard login link for the connected accountPOST /api/stripe/webhook– handlesaccount.updatedandtransfer.*events; updatespayout_accountsandpayout_statements
Testing locally:
stripe listen --forward-to localhost:3000/api/stripe/webhook
# Examples
stripe trigger account.updated
# Or resend a specific event
stripe events resend evt_123The metrics pipeline lives in the sibling repo clipper-functions/ (Supabase Edge Functions):
- Scheduler:
supabase/functions/metrics-scheduler/index.ts– periodically seeds/starts due jobs - Worker:
supabase/functions/metrics-worker/index.ts– webhook endpoint to receive Bright Data results and write metrics - Scraper client:
supabase/functions/scraper/clients/brightdata.ts - Platforms:
supabase/functions/scraper/platforms/{tiktok,instagram,facebook,youtube}.ts
Job lifecycle:
- Ensure jobs exist for tracked URLs (
ensure_metrics_jobs) - Start due
metrics_jobsin small batches; Bright Data posts back to the Worker - Worker maps rows →
{ views, likes, comments }, inserts intocreative_url_metrics, and schedules next run (complete_metrics_job) - Failures call
fail_metrics_joband can notify via webhook
Environment variables are required in the Edge Functions project; see above.
app/
api/stripe/{onboard,login-link,webhook}/route.ts
api/campaigns/*, api/tenants/* – server endpoints
dashboard/* – UI routes
components/* – UI components (Shadcn/Radix), tables, forms
hooks/* – client and server hooks (role checks, dashboard store)
lib/
supabase/{server.ts,client.ts,middleware.ts}
stripe/server.ts
api/* helpers
sql/
rbac.sql (+ rbac/*)
Scripts:
npm run dev– start Next.js dev servernpm run build– production buildnpm run start– start production servernpm run lint– run ESLint
Role capabilities are enforced via Supabase RLS and RPCs (see sql/rbac.sql). Users can hold multiple roles within the same tenant; permissions are the union of their roles. A separate platform admin table grants global operator powers.
- Approvals: approve/deny campaign rules (
approve_campaign,deny_campaign) - Memberships: invite managers/creators, promote/demote roles (
promote_user_in_tenant) - Creatives: read all, invalidate URLs (
creative_invalidate_url) - Payouts: preview, create statements, and execute (
preview_creator_payout,create_payout_statement_from_preview,mark_payout_status) - Tenants: create/manage, delete organization (secure RPC + confirmation)
- Campaigns: create campaigns, configure payout rules; view approval status
- Invites: invite creators to campaigns (
campaign_invite_creator) - Creatives: approve/deny pending creatives; invalidate violating URLs
- Payouts: preview and generate statements; execution may be admin-gated
- Join: accept campaign invites (
campaign_accept_invite) - Upload: create creatives and attach multiple URLs (
creative_upload,creative_add_url) - Metrics: view timeseries and totals for own creatives
- Payouts: view own statements; connect/update payout account (
upsert_my_payout_account)
Notes:
- Campaign uploads require campaign rules to be approved.
- URL invalidation excludes that URL’s metrics from payout math.
- Metrics ingestion is performed by background functions; delays are possible.
- Prefer Server Components; use Client Components for browser APIs, Zustand, forms
- Use
requireActiveTenant()for SSR tenant scoping; store cookieactive_tenant - Forms:
react-hook-form+ Zod; validate on client and server actions - Avoid exposing service keys in browser; use RPCs with user session and RLS
- Vercel recommended. Set the environment variables listed above.
- Configure Stripe webhook to
https://your-domain/api/stripe/webhookwithSTRIPE_WEBHOOK_SECRET. - Deploy and configure the Supabase Edge Functions in
clipper-functions/(scheduler/worker) with required env vars and public Worker URL for Bright Data datasets.
- Auth logout loops: ensure
middleware.tsusesupdateSessionand env vars are set - Webhook 400: verify
STRIPE_WEBHOOK_SECRETand that raw body is used (route handler already uses arrayBuffer) - Missing RPCs/tables: confirm
sql/rbac.sqlexecuted successfully - Metrics not updating: check Worker logs, dataset webhook URL, and service-role envs
Environment variables recap:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEYSUPABASE_SERVICE_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETNEXT_PUBLIC_APP_URLorNEXT_PUBLIC_VERCEL_URL
Local webhook testing:
stripe listen --forward-to localhost:3000/api/stripe/webhookOpen items (roadmap):
- notifications for approval/denied creatives for user-side
- notifications for payments for both creators and managers
- notifications for campaign managers about new creatives
- send email to user invited to org
- show list of pending invitations when user visits orgs page
- more robust search and filtering members
- suspense and skeleton loaders for anything that is data fetched
- allow adding/removing users within an org to a campaign (with autocomplete)
- scope the date range of the analytics to start when the user added it