A tiny, fast, SMS‑first CRM built with Next.js + Supabase. Tenant‑aware, Twilio‑enabled, and still early — APIs and UI are subject to change and not yet production‑ready.
This repo is an opinionated, open‑source reference app for an SMS‑centric CRM: capture leads from anywhere, message them in a single thread, and automate quotes/alerts with a Supabase‑native outbox worker. It’s designed to be easy to fork, easy to run locally, and easy to adapt to your own multi‑tenant use cases.
If you’re here from an open‑source downloader or template browser: this is not a generic starter. It ships a real data model, Twilio dispatcher, and guardrails for multi‑tenant SaaS apps.
- Massive simplicity: leads → messages → notifications, without a bunch of extra CRM bloat.
- Supabase‑native: RLS security, Realtime updates, SQL‑first data model, and Edge Functions/cron for background work.
- Bring Your Own Provider: Twilio adapter for real SMS, plus a fake provider so you can click around with no credentials.
- Tenant‑aware from day one: all business tables are
tenant_id‑scoped and locked down by RLS. - Contrib‑friendly: small surface area, typed domain, and Playwright smoke tests so you can refactor with confidence.
If you just want to kick the tires, you can run everything locally and use the fake SMS provider — no Twilio account required.
| Layer | Purpose | Notes |
|---|---|---|
| Next.js App Router | Dashboard, leads workspace, admin tooling | Tailwind + shadcn/ui for layout/components |
| Supabase (Postgres + Auth + Realtime) | Auth, multitenant data, RLS, realtime updates | supabase/ folder contains migrations, seeds, SQL helpers |
Supabase Edge Function dispatch-notifications |
Pulls from public.notifications, calls Twilio, logs to public.messages |
Scheduler via pg_cron + Supabase CLI |
| Twilio (optional) | Sends SMS notifications to leads/owners | Tenant-specific credentials via messaging_credentials or shared fallback env vars |
| Vitest + Playwright | Unit + E2E tests guarding guards and protected routes | npm run test:unit, npm run test:e2e |
Additional details:
- App: Next.js 16 (App Router), TypeScript, Tailwind, shadcn/ui
- Tooling: npm, Supabase CLI, Playwright, Vitest
Repo highlights:
app/– Next.js routes, protected layouts, auth flow, and the leads workspace UIcomponents/– UI and feature components (leads feed, dispatcher panel, Twilio debugger, etc.)lib/– utilities, auth guards, Supabase helperssupabase/migrations/– authoritative schema, policies, helpers, and worker/cron SQLsupabase/functions/dispatch-notifications/– Edge Function worker that pulls frompublic.notificationsand sends via Twiliosupabase/sql/– cron installer + post‑deploy sanity checksdocs/– data model and Twilio plan
The exact schema lives in supabase/migrations/, but conceptually this app revolves around:
- Leads – people you’re talking to (with flexible JSONB properties per tenant)
- Messages – inbound/outbound SMS rows tied to a lead
- Notifications – internal queue of events the dispatcher will pick up (e.g. “lead created”, “quote request received”)
- Notification rules – per‑tenant preferences for who gets alerted and how
- Messaging credentials – optional per‑tenant Twilio credentials (otherwise the app can fall back to shared env vars)
All core tables are tenant_id‑scoped and protected by RLS so each customer only sees their own data.
For a deeper walkthrough of the SMS dispatcher and queues, see docs/twilio-lead-sms-plan.md.
At a high level:
- Next.js app renders the dashboard, leads workspace, and admin tools. Server actions talk directly to Supabase using the service role key on the server only.
- Supabase Postgres stores tenants, users, leads, messages, notification rules, and Twilio credentials. RLS enforces tenant isolation everywhere.
- Supabase Edge Function
dispatch-notificationsacts as a worker: it leases rows frompublic.notifications, calls Twilio (or the fake provider), writes topublic.messages, and updates the notification state. - pg_cron (installed via
supabase/sql/install-dispatch-cron.sql) schedules the worker to run on a fixed cadence. - Twilio (optional) is the real SMS provider. You can also run with the fake provider to simulate sends in development.
You’ll need:
- Node.js 20+
- Supabase project (URL, anon/publishable key, service role key)
- Optional: Twilio Account SID + Auth Token + Messaging Service SID (or a single From number)
npm installCopy .env.local.example → .env.local and keep these values in sync with Vercel + Supabase secrets.
| Key | What it Controls | Next.js / Vercel | Supabase secret | Notes |
|---|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL | ✅ | ✅ | Also used by scripts |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY |
Browser auth key | ✅ | ||
SUPABASE_SERVICE_ROLE_KEY |
Server actions + dispatcher | ✅ | ✅ | Required by scripts + Edge Function |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN |
Shared Twilio credentials (optional) | ✅ | ✅ | Tenants can override via messaging_credentials |
TWILIO_FROM_NUMBER / TWILIO_MESSAGING_SERVICE_SID |
Default sender identity | ✅ | ✅ | Either an E.164 number or Messaging Service SID |
DISPATCH_ALERT_WEBHOOK_URL |
Slack/Teams webhook for failures | ✅ | ✅ | Optional but recommended for monitoring |
NOTIFICATION_BATCH_SIZE, NOTIFICATION_LEASE_SECONDS, NOTIFICATION_MAX_ATTEMPTS, NOTIFICATION_MAX_BACKOFF_MINUTES |
Dispatcher tuning knobs | ✅ | ✅ | Defaults: 25 / 45s / 5 attempts / 60m |
PLAYWRIGHT_BASE_URL |
Overrides smoke‑test target | ✅ | Defaults to http://127.0.0.1:4000 |
|
SEED_TEST_ADMIN_PASSWORD |
Overrides the default password for the deterministic test admin | ✅ | ✅ | Optional; defaults to TestAdmin!123 |
Next.js tip: any variable prefixed with NEXT_PUBLIC_ is bundled to the client. Never put secrets there.
With the Supabase CLI installed and logged in:
supabase start
npm run db:push
npm run seedRunning npm run seed executes supabase/seed.sql and provisions a deterministic admin account.
- Email:
test-admin@ironedge.dev - Password:
TestAdmin!123(override viaSEED_TEST_ADMIN_PASSWORD)
In one terminal:
npm run devIn another (optional, if you want the real worker running):
supabase functions serve dispatch-notifications --env-file .env.localLog in at http://localhost:3000 with the seeded admin.
Playwright smoke tests will spin up the dev server and exercise protected routes and basic workspace flows:
npm run test:e2e| Task | Command |
|---|---|
| Push latest schema to local Supabase (auto‑runs sanity check) | npm run db:push |
| Run the RLS/cron sanity report manually | npm run db:sanity |
| Seed demo data (tenant, pipeline, leads) | npm run seed |
| Install/refresh cron job | npm run cron:install |
| Deploy dispatcher + cron in one go | npm run dispatch:deploy |
| Trigger the Edge Function manually | ./scripts/dispatch-test.sh or pwsh scripts/dispatch-test.ps1 |
| Run unit tests for guards/helpers | npm run test:unit |
| Run smoke tests | npm run test:e2e |
The
scripts/dispatch-test.*helpers readNEXT_PUBLIC_SUPABASE_URL+SUPABASE_SERVICE_ROLE_KEYfrom your environment and wrap an HTTP call to the worker so you don’t have to remember the curl/PowerShell incantations.
-
Vercel hosts the Next.js frontend + server actions. Set every env var from the table above in the Vercel dashboard before deploying.
-
Supabase stores the Twilio secrets used by the Edge Function:
supabase secrets set \ TWILIO_ACCOUNT_SID=... \ TWILIO_AUTH_TOKEN=... \ TWILIO_FROM_NUMBER=... \ TWILIO_MESSAGING_SERVICE_SID=... \ DISPATCH_ALERT_WEBHOOK_URL=... -
SUPABASE_SERVICE_ROLE_KEYshould stay in Supabase/CLI and server environments only. Never expose it client‑side.
npm run dispatch:deploy # supabase functions deploy dispatch-notifications && cron installThe cron install script (supabase/sql/install-dispatch-cron.sql) uses supabase_functions.http_request so the job lives alongside your database. The relevant SQL:
select
cron.schedule(
'dispatch_notifications_worker',
'* * * * *',
$$
select
supabase_functions.http_request(
'dispatch-notifications',
jsonb_build_object(
'eventType', 'quote_request.received',
'limit', 50
),
'{}'::jsonb,
'POST'
);
$$
);After each deploy, run:
supabase db query supabase/sql/post-deploy-sanity.sqlThis checks that RLS is enabled, cron is installed, and the queue depth is healthy.
- Auth/permission guards –
tests/lib/guards.test.tsexercise centralized guard helpers via Vitest. They ensure anonymous visitors get redirected and non‑admins cannot hit admin pages. Run withnpm run test:unit. - Playwright flows –
tests/protected-routes.spec.tsandtests/leads-workspace.spec.tscover bounce logic and the deterministic leads/admin workflows seeded insupabase/seed.sql.npm run test:e2estarts the dev server on port 4000 automatically. - DB sanity SQL –
supabase/sql/post-deploy-sanity.sqlconfirms RLS is enabled on key tables (leads,notifications,notification_rules,messages,phone_numbers) and checks for the cron job + queue depth. - Edge monitoring –
dispatch-notificationsposts toDISPATCH_ALERT_WEBHOOK_URLon the first failure and when a notification hitsMAX_ATTEMPTS. Point that webhook at a Slack/Teams channel you actually watch.
- Admin debugger (UI) –
/protected/admincontains “Dispatcher health & Twilio history” panels showing queue depth, failing job count, last send timestamp, next retry window, and the latest Twilio sends pulled frompublic.messages. - Manual trigger – Use
scripts/dispatch-test.ps1/.shto fire the Edge Function on demand for debugging. - Tenant visibility toggles – Leads/dashboard visibility toggles emit toasts when flipped, and the nav hides routes server‑side per tenant visibility.
- Docs –
docs/twilio-lead-sms-plan.mdcaptures the end‑to‑end SMS architecture, dispatcher behavior, and roadmap.
Contributions are very welcome, especially around:
- improving the Twilio adapter and error mapping
- additional E2E flows for lead creation + messaging
- better admin tooling for queues and cron health
- docs for multi‑tenant patterns and security
Typical flow:
- Fork the repo and create a feature branch.
- Make changes with tests where it makes sense.
- Run
npm run test:unitandnpm run test:e2elocally. - Open a PR with a short description of the behavior change.
- Email adapter (e.g. Resend) + templates
- Web form widget for lead capture (with UTM auto‑capture)
- Attachments via Supabase Storage
- Multi‑tenant admin & audit log
- Scheduled sends and quiet‑hours controls per tenant
- Optional WhatsApp channel via Twilio or another provider
Apache License 2.0 — permissive, patent‑grant, and enterprise‑friendly. See LICENSE and NOTICE for full terms and attribution.