Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
One shared inbox, intelligent AI replies, automated lead qualification, and a pipeline built around conversations. Built on the official Meta WhatsApp Business Cloud API.
BusinessTypeScript · PLpgSQLGitHub @DevCorescentOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
AI-Powered WhatsApp CRM, Automation & Lead Management platform — Next.js 16 full-stack, Neon PostgreSQL, Groq Llama AI.
Prerequisites
• Node.js 20+
• Get the .env file from team lead (never commit it)Read the full README ↗AI-Powered WhatsApp CRM, Automation & Lead Management platform — Next.js 16 full-stack, Neon PostgreSQL, Groq Llama AI.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) — frontend + backend in one |
| Database | PostgreSQL on Neon (serverless, no setup needed) |
| ORM | Prisma v7 |
| Auth | NextAuth v5 — credentials + JWT (DONE) |
| AI | Groq — Llama 3.3 70B |
| Real-time | Pusher (WebSocket for live inbox) |
| Validation | Zod |
| UI | Tailwind CSS v4 + Lucide Icons |
| State / Data | TanStack React Query v5 |
.env file from team lead (never commit it)git clone <repo-url>
cd whatsapp_lead
npm install
npm run db:generate
npm run dev
npm run db:seed
Demo login: [email protected] / Demo@1234
| Variable | Description |
|---|---|
DATABASE_URL | Neon PostgreSQL — already set, do not change |
AUTH_SECRET | NextAuth JWT secret |
GROQ_API_KEY | Groq AI key — already set |
GROQ_MODEL | llama-3.3-70b-versatile |
WHATSAPP_PHONE_NUMBER_ID | Meta WhatsApp number ID (per tenant in DB) |
WHATSAPP_VERIFY_TOKEN | Webhook verification token |
WHATSAPP_APP_ID | Meta app ID — required for Embedded Signup |
WHATSAPP_APP_SECRET | Meta app secret — webhook HMAC and Embedded Signup code exchange. Server only |
WHATSAPP_ES_CONFIG_ID | Facebook Login for Business configuration ID for Embedded Signup |
WHATSAPP_API_VERSION | Graph API version. Defaults to v19.0; Meta recommends v25.0 for Embedded Signup |
WHATSAPP_REGISTER_PIN | Optional 6-digit 2FA PIN used to register newly onboarded numbers. Registration is skipped when unset |
ENCRYPTION_KEY | AES-256-GCM key for secrets at rest (access tokens, app secrets) |
PUSHER_APP_ID / PUSHER_KEY / PUSHER_SECRET | Pusher real-time |
NEXT_PUBLIC_APP_URL | http://localhost:3000 |
WHATSAPP_APP_SECRETmust never be exposed to the browser. The app ID and the Embedded Signup config ID reach the client through/api/integrations/whatsapp/config, which is authenticated and returns no secrets — do not addNEXT_PUBLIC_*copies.
Customers connect WhatsApp through Meta's official
Embedded Signup flow
rather than by pasting credentials. Settings → WhatsApp (and each card on
/businesses) shows a Continue with Facebook button that runs Meta's dialog;
the result is verified server-side and saved as a WhatsAppIntegration of the
business — one row per connected number, so a business can connect several.
whatsapp_business_management and whatsapp_business_messaging.WHATSAPP_ES_CONFIG_ID.<NEXT_PUBLIC_APP_URL>/api/webhook/whatsapp
with WHATSAPP_VERIFY_TOKEN as the verify token. Embedded Signup subscribes each
customer's WABA to this app, so every customer's traffic arrives at that one URL,
signed with WHATSAPP_APP_SECRET. Customers never configure a webhook themselves.| Step | Where | Graph call |
|---|---|---|
| Customer authorises, picks WABA + number | Meta's dialog | Facebook Login for Business |
| Exchange the 30-second code for a business token | /api/integrations/whatsapp/connect | GET /oauth/access_token |
| Confirm which WABAs the token really covers | same | GET /debug_token → granular_scopes[].target_ids |
| Confirm the phone number is on that WABA | same | GET /<WABA_ID>/phone_numbers |
| Point the customer's WABA at our webhook | same | POST /<WABA_ID>/subscribed_apps |
| Register the number for Cloud API | same | POST /<PHONE_NUMBER_ID>/register |
The waba_id and phone_number_id the browser reports are treated as hints and
re-derived from Meta before anything is written — see selectWaba() /
selectPhoneNumber() in lib/whatsappEmbeddedSignup.ts and the tests in
tests/whatsapp-embedded-signup.test.ts.
Manual credential entry is still available under Settings → WhatsApp → Manual setup
for workspaces that predate this flow and for deployments whose Meta app is not yet
approved. Hand-entered credentials are mirrored into a WhatsAppIntegration row
whenever they are saved, so they behave like any other connected number.
WhatsAppIntegration is the runtime source of truth (lib/whatsappIntegrations.ts,
rules in lib/whatsappIntegrationRules.ts):
| Path | Which number |
|---|---|
| Inbound webhook | metadata.phone_number_id → its active WhatsAppIntegration → tenant + business. A disconnected number is ignored. Numbers with no row fall back to the legacy Business/TenantSettings lookup. |
| Conversations | One thread per contact per number; Conversation.whatsappIntegrationId records it. |
| Manual replies, AI, chatbot flows, FAQ, opt-out, read receipts | The conversation's own number (resolveConversationWhatsAppCreds). If that number is disconnected the send is refused — never re-sent from a different number. |
| Campaigns, templates | The business's default number (resolveWhatsAppCreds), then legacy credentials. |
| Test / Disconnect | Per number (integrationId). Disconnect keeps the row (inactive, token wiped) and promotes the next number to default. |
Guarantees: a phone_number_id is globally unique (unique index; concurrent
connects of the same number get a 409), and a business has at most one default
(row lock + partial unique index).
prisma/migrations/20260918120000_whatsapp_integrations/migration.sql
(idempotent; safe on a database that already has the table from db push).
It refuses to run if two rows share a phoneNumberId.
prisma db push does not know the partial "one default per business" index and
may drop it — re-apply the migration file afterwards.npm run db:backfill-whatsapp -- --dry-run, review, then npm run db:backfill-whatsapp
to mirror existing hand-entered numbers and attach their conversations.whatsapp_lead/
├── app/
│ ├── (marketing)/ # Landing pages — AMAN
│ ├── (auth)/ # Login / Register — HEMANT (UI) + SHALMON (logic)
│ ├── (dashboard)/ # Workspace UI — HEMANT (UI) + GAURANSH (APIs)
│ ├── (admin)/ # Super-admin panel — HEMANT (UI) + SHALMON (APIs)
│ └── api/
│ ├── auth/ # NextAuth + register — SHALMON ✅ DONE
│ ├── contacts/ # CRM CRUD — SHALMON
│ ├── conversations/ # Inbox — GAURANSH
│ ├── messages/ # Send WA message — GAURANSH
│ ├── leads/ # Lead pipeline — GAURANSH
│ ├── campaigns/ # Bulk send — GAURANSH
│ ├── tickets/ # Support — GAURANSH
│ ├── templates/ # WA templates — GAURANSH
│ ├── knowledge/ # KB docs — GAURANSH
│ ├── analytics/ # KPIs — GAURANSH
│ ├── ai/ # AI endpoints — GAURANSH
│ ├── webhook/ # WA webhook — GAURANSH
│ └── admin/ # Platform admin — SHALMON
├── lib/
│ ├── auth.ts # NextAuth config ✅ DONE
│ ├── prisma.ts # DB client ✅ DONE
│ ├── ai.ts # Groq helpers ✅ DONE
│ ├── whatsapp.ts # WA Cloud API client
│ └── validators/ # Zod schemas ✅ DONE
├── hooks/ # React Query hooks (Hemant uses these)
├── types/ # Shared TypeScript types
├── components/ # UI components per intern
└── prisma/
├── schema.prisma # 16-model schema ✅ DONE
└── seed.ts # Plans + demo data ✅ DONE
| Intern | Branch | Work Area |
|---|---|---|
| Shalmon | feature/shalmon-auth-crm | Auth APIs, Contacts API, Admin APIs |
| Aman | feature/aman-landing-marketing | All marketing/landing pages |
| Hemant | feature/hemant-dashboard-inbox | All dashboard + admin UI |
| Gauransh | feature/gauransh-whatsapp-leads | WhatsApp, AI, Leads, Campaigns |
# Start every session
git checkout feature/<your-name>-...
git fetch origin && git rebase origin/master
Branch: feature/shalmon-auth-crm
Auth and registration are already done. Focus on contacts CRUD and admin APIs.
GET /api/contacts — list contacts for tenant with pagination (page, limit)
search (name or phone ILIKE)tagId{ data: Contact[], pagination: { page, limit, total } }POST /api/contacts — create new contact
createContactSchema from lib/validators/contact.tstenantIdGET /api/contacts/[id] — single contact with tags + linked lead + conversation countPATCH /api/contacts/[id] — partial update (name, email, company, notes, tags)DELETE /api/contacts/[id] — soft delete: set isActive: falsehooks/useContacts.ts — add useCreateContact, useUpdateContact, useDeleteContact mutationsGET /api/admin/tenants — list all tenants (name, plan, userCount, isActive, joinedAt)
search, isActive, planId query paramsPATCH /api/admin/tenants/[id] — suspend (isActive: false) or change planGET /api/admin/stats — platform-wide KPIs:
totalTenants, activeTenants, totalUsers, totalMessages, MRRsignupsLast30Days: [{ date, count }] for chartGET /api/admin/plans — list plans with subscriber count (already has skeleton)// ALWAYS include this — never skip
where: { tenantId: session.user.tenantId, ... }
Branch: feature/aman-landing-marketing
All files are in
app/(marketing)/. Build full UI — no backend work needed.
components/marketing/Navbar.tsx)sticky top-0 z-50 bg-white/95 backdrop-blur)app/(marketing)/page.tsx)app/(marketing)/pricing/page.tsx)features/page.tsx — expand on 6 features with screenshots/mockupscontact/page.tsx — name, email, message form (no API needed, just UI)about/page.tsx — company story, team sectionindustries/page.tsx — 6 industry cards (Real Estate, EdTech, E-commerce, etc.)privacy-policy/page.tsx, terms/page.tsx, refund-policy/page.tsx — plain text contentcomponents/marketing/Footer.tsx)#6C3FC4 (purple)sm:, md:, lg: prefixes)Branch: feature/hemant-dashboard-inbox
Build all UI. Use React Query hooks from
hooks/for data. Handle loading and empty states.
components/dashboard/Sidebar.tsx)components/dashboard/Topbar.tsx)app/(dashboard)/inbox/page.tsx) — 3 columnsapp/(dashboard)/contacts/page.tsx)/contacts/[id]app/(dashboard)/contacts/[id]/page.tsx)app/(dashboard)/leads/page.tsx) — Kanban boardapp/(dashboard)/analytics/page.tsx)campaigns/page.tsx — table of campaigns + "Create Campaign" buttonchatbot/page.tsx — visual flow builder placeholdertickets/page.tsx — ticket table with status/priority filtersknowledge-base/page.tsx — doc list + upload buttonai-settings/page.tsx — AI toggle, system prompt textarea, auto-reply settings formteam/page.tsx — agent list table + invite buttonsettings/page.tsx — tabs: General | WhatsApp | Billing | Notifications(admin)/layout.tsx — dark sidebar (already scaffolded, make it functional with links)(admin)/dashboard/page.tsx — KPI cards + 2 charts (already scaffolded, make it look real)(admin)/tenants/page.tsx — complete the table with real columns and filter UI(admin)/plans/page.tsx — 3 plan cards with edit modal(admin)/revenue/page.tsx — MRR/ARR cards + line chart// Use lucide-react for ALL icons
import { MessageSquare, Users, TrendingUp } from "lucide-react";
// Conditional classes
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
const cn = (...c) => twMerge(clsx(c));
// Handle loading state from hooks
const { data, isLoading } = useContacts();
if (isLoading) return <SkeletonLoader />;
Branch: feature/gauransh-whatsapp-leads
All backend APIs. AI functions in
lib/ai.tsare already implemented. Focus on the route handlers.
app/api/webhook/whatsapp/route.ts)WAWebhookPayload (see types/index.ts for the shape)from (phone), messageId, text.body, timestamp, typeprisma.contact.upsert({ where: { phone_tenantId }, create: {...} })status: "OPEN")direction: "INBOUND", status: "DELIVERED")markMessageAsRead() from lib/whatsapp.tspusher.trigger("tenant-${tenantId}", "new-message", savedMessage)tenant.settings.aiAutoReply === true:
generateReply(messages, tenant.settings.aiSystemPrompt) from lib/ai.tssendTextMessage() → save outbound message → broadcastPOST /api/messages){ conversationId, content, type, isNote? }sendMessageSchemaisNote: save with isNote: true, no WhatsApp sendsendTextMessage(phoneNumberId, apiKey, to, content)direction: "OUTBOUND", status: "SENT"app/api/conversations/)GET /api/conversations — list conversations for tenant
status (OPEN/RESOLVED), assigneeIdupdatedAt descGET /api/conversations/[id] — conversation + messages (paginated, 50 at a time)PATCH /api/conversations/[id] — update status or assigneeIdapp/api/leads/)GET /api/leads — return leads grouped by stage for Kanban:
// Return format Hemant's Kanban expects:
{ NEW_LEAD: Lead[], CONTACTED: Lead[], QUALIFIED: Lead[], ... }
POST /api/leads — create lead, link to contactIdGET /api/leads/[id] — lead details + activities timelinePATCH /api/leads/[id] — update stage/score/value/assignee
LeadActivity { type: "STAGE_CHANGED", oldValue, newValue }scoreLabel from scoreDELETE /api/leads/[id] — soft deleteapp/api/campaigns/)GET /api/campaigns — list with stats (sent count, failed count)POST /api/campaigns:
{ name, message, contactIds[] } (simple text message, no template needed for now)Campaign record + CampaignContact rowssendTextMessage() for eachCampaignContact.status to "SENT" or "FAILED"status: "COMPLETED" when loop finishesapp/api/ai/)POST /api/ai/qualify — body: { conversationId }
qualifyLead(text) from lib/ai.tsscore, scoreLabel, bantBudget, bantAuthority, bantNeed, bantTimeline{ score, scoreLabel, bantBudget, bantAuthority, bantNeed, bantTimeline, reasoning }POST /api/ai/summarize — body: { conversationId }
summarizeConversation(messages) from lib/ai.ts{ summary: string }POST /api/ai/reply — body: { conversationId, systemPrompt? }
generateReply(), return { reply: string }GET /api/analytics)period (7d / 30d / 90d){
totalConversations: number,
openConversations: number,
resolvedToday: number,
totalMessages: number,
avgResponseTimeMinutes: number,
totalLeads: number,
wonDeals: number,
conversionRate: number,
messagesChart: { date: string, sent: number, received: number }[],
leadsByStage: { stage: string, count: number }[]
}
app/api/tickets/, app/api/templates/, app/api/knowledge/)GET + POST /api/tickets — CRUD with status/priority filtersPATCH /api/tickets/[id] — update status, assigneeGET + POST /api/templates — list and save WA message templatesGET + POST /api/knowledge — list and save knowledge base documentsDELETE /api/knowledge/[id] — delete dochooks/useMessages.ts — useSendMessage() mutation, useResolveConversation() mutationhooks/useLeads.ts — useCreateLead(), useUpdateLeadStage() with optimistic update// Never forget tenantId or you will leak data across workspaces
const leads = await prisma.lead.findMany({
where: { tenantId: session.user.tenantId }
});
Lead stages: NEW_LEAD → CONTACTED → QUALIFIED → PROPOSAL_SENT → NEGOTIATION → WON | LOST
Score labels: COLD 0-30 | WARM 31-60 | HOT 61-80 | QUALIFIED 81-100
User roles: SUPER_ADMIN | TENANT_OWNER | ADMIN | AGENT
Message directions: INBOUND (from customer) | OUTBOUND (from agent/AI)
# Commit format
git commit -m "feat(contacts): implement GET with pagination and tag filter"
# Prefixes: feat | fix | refactor | style | docs
# Scopes: auth | contacts | inbox | leads | campaigns | ai | webhook | admin | ui | marketing
Merge order when submitting PR:
| URL | http://localhost:3000 |
| [email protected] | |
| Password | Demo@1234 |
| Role | Tenant Owner |