Open-Source Cold Email & Warmup Platform
Warmbly는 메일함을 예열하고 캠페인 전송·답변 triage를 지원하는 오픈소스 콜드 이메일 플랫폼입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Multi-tenant SaaS for real-estate teams and agencies: Meta ads reporting, a leads
pipeline, and a WhatsApp bot that welcomes and qualifies every new lead. It replaces the single-firm meta-crm
app. The full plan is in docs/PLATFORM_PLAN.md.
Stack: Next.js 16 (App Router, Server Components/Actions), Supabase (Postgres + Auth + RLS + Vault), Inngest (background jobs), Tailwind 4.
| Area | Status |
|---|---|
| Sign up, sign in, sign out, email confirmation | ✅ |
| Workspaces (organizations), switcher, 14-day trial banner | ✅ |
| Roles: owner, admin, manager, agent, viewer, enforced by Postgres RLS | ✅ |
| Invite links (hashed, single use, 7-day expiry, bound to email) | ✅ |
| Team page: change role, remove member, revoke invite | ✅ |
| Projects | ✅ |
| Leads: add, filter, search, keyset pagination, status, assignment, timeline events | ✅ |
| Overview: 7/30/90-day range, project filter, campaign + ad tables with Wasting/Best flags | ✅ (tables need migration 0003) |
| Lead detail: call / WhatsApp / email, form answers, timeline with notes | ✅ |
| Project access for managers and viewers | ✅ |
| Leads CSV export (admins, logged, formula-injection safe) | ✅ |
| Account page: name, password, two-factor login (authenticator app) | ✅ |
| Client report links: read-only dashboard, no login, expiring, revocable | ✅ (needs migration 0004) |
| Platform admin: suspend / reactivate workspaces (two-factor required) | ✅ (needs migration 0004) |
| Platform admin: usage, health page, logged read-only support view (customers see visits) | ✅ (needs migrations 0013, 0014) |
| Audit log (append-only) | ✅ |
| Landing page | ✅ |
| System / Light / Dark appearance, mobile layout, nonce-based CSP | ✅ |
| Meta connect (System User token in Vault), ad account + Page picker | ✅ |
| Ads + leads sync jobs (Inngest), instant lead webhook with inbox + sweeper | ✅ |
| Facebook Login (OAuth) connect | ⏳ after Meta App Review |
| WhatsApp bot per workspace: welcome template, button questions, scoring, AI answers from your facts, follow-ups | ✅ (needs migration 0005 + a number) |
| WhatsApp inbox in two sections, "We messaged" and "Messaged us", with pause-the-bot and manual replies | ✅ |
| Trial expiry: workspace pauses (leads keep arriving, CSV export still works); admin can extend or set a plan | ✅ (needs migration 0006) |
| Forgot password / reset by email | ✅ (Supabase's built-in email until Resend is set up) |
| Onboarding: setup guide for new workspaces, first-visit product tour, help center with step-by-step guides, landing FAQ | ✅ (guides live in src/lib/guides.ts) |
| Privacy policy, terms, data-deletion pages | ✅ (fill in LEGAL in src/lib/brand.ts) |
| Billing (Stripe) | ⏳ needs a Stripe account (India is invite-only: register the company first) |
| Daily / weekly email reports | ⏳ needs a Resend account |
supabase/migrations/ in order (0001, 0002, ...).
Then run supabase/tests/rls_isolation.sql. It should end with
ALL ... RLS ISOLATION CHECKS PASSED (that "error" is how it rolls its test data back).cp .env.example .env.local
npm install
npm run dev
For local development you can turn off Authentication → Providers → Email → Confirm email in Supabase, so sign-up logs you straight in.
supabase/migrations/0002_meta_integration.sql in the SQL Editor..env.local, fill in the two server-only secrets (see .env.example):
SUPABASE_SECRET_KEY (Supabase → Project Settings → API keys) and META_APP_SECRET (Meta app → Settings → Basic).npm run dev:
npm run inngest:dev
Its dashboard (http://localhost:8288) shows every sync run, retry and failure.
Local jobs are locked while this PC shares the live database: start the app with
LEADOS_ALLOW_LIVE_DB=yes npm run dev to allow them for that run (see Tests below).leadgen) at
https://<your-domain>/api/webhooks/meta with META_WEBHOOK_VERIFY_TOKEN as the verify token.
Locally, leads still arrive through the scheduled sync.A pasted token belongs to whichever Meta app it was generated for. For your own business that's our app, so webhooks reach us. A client's token comes from their app, so their instant leads and WhatsApp replies go there instead; their leads still arrive through the hourly sync. "Continue with Facebook" (Facebook Login for Business, after Meta App Review) fixes this.
How syncing works:
| Job | When | What |
|---|---|---|
meta-schedule-syncs | hourly (IST :05), deep at 02:40 and 14:40 | Fans out one ads sync per enabled account (2 days, or 7 on deep runs) and one leads sync per workspace |
meta-ads-sync | per event | One run per ad account at a time; app-wide throttle. Upserts only changed rows, then rebuilds that project's rollups |
meta-leads-sync | per event | Pulls leads for ads that delivered in the last 14 days; existing leads keep their status |
meta-lead-received | per webhook | Separate lane so instant leads never wait behind syncs |
meta-sweep-webhooks | every 5 min | Re-queues webhook events that were stored but not processed |
meta-rollups-rebuild | per event | Rebuilds a workspace's project totals after an ad account is linked, moved or unlinked |
wa-* | per event / hourly | Welcome, inbound messages (one at a time per phone), manual replies, follow-ups 10:15–19:15 IST, account linking |
When Meta rejects a token, the workspace is marked Reconnect needed and its jobs stop retrying. When Meta's usage headers pass 75%, the job backs off until Meta says the quota is back.
supabase/migrations/0005_whatsapp_bot.sql.subscribed_apps), so their replies reach the app the token belongs to (see the note under Meta). If the token can't
see the account, the page says so and offers Check again.{{1}} = first name and
{{2}} = project; give the welcome quick-reply buttons like "Yes, let's start", "Call me", "Not interested".https://<your-domain>/api/webhooks/meta with META_WEBHOOK_VERIFY_TOKEN, and subscribe the
messages field.GEMINI_API_KEY lets the bot understand typed messages and answer questions from your facts.WA_DRY_RUN=1 (development only) logs messages instead of sending them. The bot's decisions live in
src/server/bot/brain.ts (pure, unit-tested); sending and saving in runner.ts.
Run supabase/migrations/0006_billing_guard.sql. It also stops
workspace admins from changing their own plan, trial date or suspension through the API (only name stays
editable). When a trial ends, every page of the workspace goes to /o/<slug>/trial-ended. Lead capture, syncs
and the CSV export keep working, and the bot finishes conversations already in progress but starts no new
ones. Until Stripe is wired in, set a paid plan or extend a trial in /admin.
Run once in the SQL Editor, using the email you signed up with:
insert into public.platform_admins (user_id, role)
select id, 'super_admin' from auth.users where email = '[email protected]';
A "Platform admin" link then appears in the sidebar (/admin).
A production build (next build) fails, and a production server refuses to start, if a required setting is
missing or a dev-only switch is on; the error names the setting, never its value. So next build on a dev
machine with .env.local fails on purpose: build for production on the host. Set these in the host's environment:
| Variable | Notes |
|---|---|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Production Supabase project |
SUPABASE_SECRET_KEY | Required. Server only |
META_APP_SECRET, META_WEBHOOK_VERIFY_TOKEN | Required |
NEXT_PUBLIC_SITE_URL | Required: only the domain, e.g. https://leados.in (links, sitemap). Set before the build |
INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY | Required. From Inngest Cloud |
GEMINI_API_KEY | Optional |
WA_DRY_RUN, INNGEST_DEV | Must not be set (any value other than 0/false blocks the build) |
GET /api/health returns 200 {"ok":true} when the database answers and 503 {"ok":false} otherwise. Point an
uptime monitor at it./robots.txt and /sitemap.xml are generated from src/lib/seo.ts..github/workflows/ci.yml runs type-check, lint and unit tests on every push. To block merging while it
is red: GitHub → Settings → Branches → add a rule for main → Require status checks to pass → pick check.org_id and RLS.requireOrg), never from an id sent by the browser.SECURITY DEFINER RPCs with a pinned search_path: create_organization, create_invitation, accept_invitation, set_member_role, remove_member, write_audit.org_id can never change after insert.webhook_events and sync_runs writes are worker-only (no RLS policy).src/server/supabase-admin.ts) is importable only from src/server/** and src/app/api/**. ESLint fails the build otherwise.connect_meta() / connect_whatsapp() into Supabase Vault. Only worker_meta_token() / worker_wa_token() (service role) can decrypt them, and job steps never return a token, so it is never persisted in Inngest.src/app/(auth)/ login, signup, forgot/reset password, server actions
src/app/o/[slug]/ workspace: overview, setup, leads, conversations, projects, team, settings + actions.ts
src/app/admin/ platform console
src/app/help/ public help center (content in src/lib/guides.ts)
src/app/(legal)/ privacy, terms, data deletion (details in LEGAL, src/lib/brand.ts)
src/app/api/ Meta/WhatsApp webhook, Inngest endpoint
src/server/jobs/ background jobs (Meta sync, WhatsApp)
src/server/bot/ WhatsApp bot: brain (pure), runner, Cloud API client, AI guard
src/server/meta/ Meta Graph API client
src/lib/auth.ts requireUser, requireOrg, atLeast (UI-only checks)
src/lib/supabase/ server client + session-refresh proxy
src/components/ UI primitives (tokens in globals.css)
supabase/migrations/ SQL, run in order
| Symptom | Fix |
|---|---|
Every page redirects to /setup | .env.local is missing or the key is malformed. Restart npm run dev after editing. |
| Sign-up says "check your inbox" but no email arrives | Supabase's built-in mailer is rate-limited. Turn off email confirmation for development, or set up SMTP. |
| Creating a workspace fails | Check that the migration ran. create_organization must exist. |
| "Saved, but the background worker isn't reachable" | Start npm run inngest:dev. |
A page under /o/... shows 404 in dev after adding routes | Stop the dev server, delete .next/dev, start it again. |
| Times look hours off | Check your computer's clock is set automatically. |
| Invite says "different email" | The invited person must sign in with exactly the address the invite was sent to. |
npm test # unit tests (also run by CI)
npm run typecheck # generates route types, then tsc
npm run test:int # integration tests against the running app: start `npm run dev` first
Safety lock. This PC shares its Supabase database with the live site, so integration tests and local background jobs refuse to run unless you allow that one run. Type the switch before the command (don't put it in any
.envfile: the lock refuses it there). Usenpm run devinstead ofnpm run test:intfor local jobs.
- Git Bash:
LEADOS_ALLOW_LIVE_DB=yes npm run test:int- PowerShell:
$env:LEADOS_ALLOW_LIVE_DB="yes"; try { npm run test:int } finally { Remove-Item Env:LEADOS_ALLOW_LIVE_DB }
Unit tests (Node's built-in runner) cover the WhatsApp bot's decisions, the AI reply guard, phone number
formatting, the waste/best flags and CSV encoding.
Tenant isolation is tested in the database by supabase/tests/rls_isolation.sql.
ad_daily uses only its default partition; nothing creates monthly partitions yet. Fine at current volume.LEGAL in src/lib/brand.ts holds placeholders; fill in the company details and have a lawyer review the pages.