A multi-tenant CRM SaaS built with Next.js (App Router), Supabase (Postgres + Auth + RLS), TanStack Query, and Tailwind. Includes a Meta (Facebook/Instagram) Lead Ads webhook that ingests leads directly into the pipeline.
- Framework: Next.js 16 (App Router, Server Actions)
- Database/Auth: Supabase (Postgres, Row-Level Security, multi-workspace membership)
- Data fetching: TanStack Query (client cache + optimistic updates)
- UI: Tailwind CSS, base-ui/Radix components, Framer Motion, dnd-kit (pipeline board)
- Email: Resend
npm install
npm run devOpen http://localhost:3000.
Create .env.local (see src/lib/env.ts for validation):
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
yes | Supabase anon key (client) |
SUPABASE_SERVICE_ROLE_KEY |
yes | Service role key (server only — webhook/admin) |
META_VERIFY_TOKEN |
webhook | Token used during Meta webhook subscription handshake |
META_APP_SECRET |
webhook | Meta app secret — verifies X-Hub-Signature-256 payload signatures |
CRON_SECRET |
retry | Bearer secret protecting the webhook retry endpoint |
RESEND_API_KEY |
Resend API key | |
RESEND_FROM_EMAIL |
From address for transactional email | |
NEXT_PUBLIC_APP_URL |
no | App base URL (defaults to localhost) |
Run the SQL files in supabase/ in the Supabase SQL Editor, in this order:
schema.sql— core tables, RLS, signup triggermulti-workspace.sql—workspace_members+ multi-workspace RLSteam-invitations.sql— invitationsnotifications.sql— notificationsmeta-integrations.sql— Meta page tokens per workspacewebhook-retry-queue.sql— failed-lead retry queueperformance-indexes.sql— composite/GIN indexes for hot query paths
The various fix-*.sql files are historical RLS patches; apply only if migrating an older instance.
Note on types:
src/lib/supabase/database.types.tsis hand-maintained. Each table must include aRelationshipsfield or the typed Supabase client degrades every table tonever. Keep it in sync when you change the schema (or regenerate withsupabase gen types typescript).
GET /api/webhooks/meta — Subscription verification.
Meta sends hub.mode, hub.verify_token, hub.challenge; the handler echoes the challenge when the token matches META_VERIFY_TOKEN.
POST /api/webhooks/meta — Lead delivery.
- Verifies the
X-Hub-Signature-256HMAC-SHA256 of the raw body againstMETA_APP_SECRET(rejects unsigned/forged payloads with403). - For each
leadgenchange, fetches the full lead from the Graph API using the page token inmeta_integrations, then creates/updates aContact(statusLead) and logs an activity. - Failures are routed to the
meta_webhook_failuresqueue: transient errors (5xx, 429, expired token190, network/DB) are retried with exponential backoff; config errors (missing integration) are not.
POST /api/webhooks/meta/retry — Re-processes due queue entries.
Requires Authorization: Bearer <CRON_SECRET>. Wire to a scheduler (e.g. Vercel Cron) to drain the retry queue periodically.
- In Graph API Explorer, generate a token with
pages_show_list,pages_read_engagement,pages_manage_metadata,leads_retrieval(requires the Marketing API product added to the app). GET /me/accounts→ copy the target page'saccess_token.POST /{page_id}/subscribed_appswithsubscribed_fields=leadgenusing the page token.- Insert the page token into
meta_integrationsfor the workspace. - Test via the Lead Ads Testing Tool.
src/
├── app/
│ ├── api/webhooks/meta/ # Meta webhook + retry endpoints
│ ├── actions/ # Server Actions (contacts, workspaces, ...)
│ ├── leads/ # Pipeline board page
│ ├── contacts/ # Contacts table + detail
│ ├── error.tsx loading.tsx # Route-level boundaries
│ └── global-error.tsx # Root-layout error boundary
├── components/
│ ├── providers/ # TanStack Query provider
│ ├── leads/ # Pipeline board (dnd-kit)
│ └── ui/ # Design system primitives
├── hooks/ # use-contacts (React Query), use-toast
└── lib/
├── env.ts # Env validation (server-only)
├── meta/process-lead.ts # Lead ingestion + retry queue logic
└── supabase/ # client/server/admin + database.types.ts
npm run dev— dev servernpm run build— production build (type errors fail the build)npm run start— serve production buildnpm run lint— ESLint
Deploy on Vercel. Set all environment variables in the project settings, and add a Cron entry hitting /api/webhooks/meta/retry (with CRON_SECRET) to drain the lead retry queue.