Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
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)檢視完整 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 |