Self-hostable CRM template for WhatsApp — shared inbox, contacts, sales pipelines, broadcasts, and no-code automations. Fork it, brand it, host it.
Go to file
Arnas Donauskas 7bd112f61c
fix(whatsapp): register phone numbers on Meta + verify subscription state
Closes the silent-failure bug a user reported: when multiple phone
numbers are connected to the same WABA, only the first one actually
receives webhook events from Meta. The wacrm UI shows "Connected"
because the metadata probe succeeds, but Meta's activity log is
empty for every subsequent number.

Root cause
----------
The POST /api/whatsapp/config handler verified credentials with a
GET /{phone_number_id} probe and saved them to Supabase — but
never called Meta's two registration endpoints that actually wire
a number for inbound events:

  1. POST /{phone_number_id}/register
       Subscribes the number to THIS app's webhook. Requires a
       6-digit 2FA PIN the user previously set in Meta WhatsApp
       Manager → Two-step verification. Without it, Meta routes
       events for that number to whichever app last claimed it.

  2. POST /{waba_id}/subscribed_apps
       Subscribes the WABA itself to this app. Required once per
       WABA, but idempotent on Meta's side so we call on every
       save without harm.

The first number "worked" because someone (the user, Embedded
Signup, or Meta dashboard) had already done both calls for it
out-of-band. Every subsequent number silently failed.

What this PR does
-----------------
* New helpers in meta-api.ts: registerPhoneNumber (treats Meta's
  "already registered" 400 as success), subscribeWabaToApp
  (idempotent), getSubscribedApps (for the diagnostic).

* POST /api/whatsapp/config now:
    - Accepts an optional `pin` (6-digit, validated).
    - Calls subscribeWabaToApp on every save (idempotent).
    - Calls registerPhoneNumber on first save and whenever a PIN
      is supplied. Skipped when the same number is already
      registered locally and no fresh PIN is provided — re-running
      /register with a stale PIN would actually undo the active
      subscription.
    - Persists registered_at / subscribed_apps_at timestamps and
      last_registration_error for diagnostic surfacing.
    - Returns {success, registered, registration_error} so the UI
      can distinguish "creds valid" from "actually live".
    - Saves the row even when /register fails so the user can
      retry without re-entering everything.

* New diagnostic GET /api/whatsapp/config/verify-registration —
  hits Meta to confirm the saved number is reachable + the WABA
  is subscribed. Returns a per-check breakdown the UI renders.
  Solves the misleading-green-banner UX failure that caused the
  bug to go unnoticed in the first place.

* Migration 015 adds registered_at, subscribed_apps_at,
  last_registration_error to whatsapp_config. All nullable so
  existing rows survive — the UI labels them "registration status
  unknown" until the user clicks Verify or re-saves.

* whatsapp-config.tsx:
    - New PIN input with help text pointing to the exact Meta
      Manager setting and explaining why it's needed.
    - Two-tier status: existing "Credentials valid" alert (relabeled
      from "Connected" since it was misleadingly broad) PLUS a new
      Registration Status alert that flips emerald/amber based on
      the registered_at timestamp.
    - "Verify with Meta" button on the registration alert pings
      the diagnostic endpoint and renders the per-check result
      inline. Surfaces last_registration_error verbatim with a
      pointer to retry.
    - PIN clears from state after a successful save so it can't
      accidentally retrigger /register on the next save.

Tests + checks
--------------
* 9 new fetch-mock tests cover registerPhoneNumber (happy path,
  already-registered passthrough, PIN-required error verbatim,
  generic Meta error), subscribeWabaToApp (URL + bearer + error),
  getSubscribedApps (data, empty, error).
* 265/265 tests pass. Lint 0 errors. Typecheck clean. Build
  registers both /api/whatsapp/config and
  /api/whatsapp/config/verify-registration.

Out of scope
------------
* Multi-number-per-CRM-user (current schema is UNIQUE(user_id)
  so one CRM user = one number). The reported bug was
  multi-number-per-WABA across multiple CRM users, which this
  fully resolves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 18:12:38 +02:00
.github test: foundational Vitest suite for security + critical pure logic 2026-05-18 16:06:02 +03:00
public feat(inbox): WhatsApp-style doodle background in the chat area 2026-05-22 10:06:34 +03:00
src fix(whatsapp): register phone numbers on Meta + verify subscription state 2026-05-29 18:12:38 +02:00
supabase/migrations fix(whatsapp): register phone numbers on Meta + verify subscription state 2026-05-29 18:12:38 +02:00
.editorconfig chore: contributor experience — CoC, templates, Dependabot, editor configs 2026-04-20 21:16:21 +03:00
.env.local.example feat(templates): submit-to-Meta endpoint + component-based form builder 2026-05-29 15:34:58 +02:00
.gitignore fix(webhook): require META_APP_SECRET, fail closed 2026-04-20 18:54:52 +03:00
.prettierignore chore: contributor experience — CoC, templates, Dependabot, editor configs 2026-04-20 21:16:21 +03:00
.prettierrc chore: contributor experience — CoC, templates, Dependabot, editor configs 2026-04-20 21:16:21 +03:00
AGENTS.md Initial commit from Create Next App 2026-04-16 17:52:39 +03:00
CHANGELOG.md docs: changelog for 0.2.1 (whatsapp_config single-tenant fix) 2026-05-26 11:26:17 +02:00
CLAUDE.md Initial commit from Create Next App 2026-04-16 17:52:39 +03:00
components.json feat: initial commit 2026-04-16 17:53:27 +03:00
CONTRIBUTING.md chore: rename product to "CRM Template for WhatsApp" 2026-05-06 11:52:11 +03:00
eslint.config.mjs Initial commit from Create Next App 2026-04-16 17:52:39 +03:00
LICENSE chore: OSS metadata + LICENSE + SECURITY + complete env example 2026-04-20 20:41:14 +03:00
next.config.ts security: baseline response headers (CSP report-only + OWASP set) 2026-04-20 19:24:41 +03:00
package-lock.json chore(deps): bump lucide-react from 1.8.0 to 1.16.0 2026-05-26 13:27:34 +00:00
package.json chore(deps): bump lucide-react from 1.8.0 to 1.16.0 2026-05-26 13:27:34 +00:00
postcss.config.mjs Initial commit from Create Next App 2026-04-16 17:52:39 +03:00
README.md docs: add Deploy on Hostinger button to README 2026-05-19 17:04:25 +03:00
tsconfig.json Initial commit from Create Next App 2026-04-16 17:52:39 +03:00
vitest.config.ts test: foundational Vitest suite for security + critical pure logic 2026-05-18 16:06:02 +03:00

wacrm — CRM Template for WhatsApp

Self-hostable CRM template for WhatsApp® — shared inbox, contacts, sales pipelines, broadcasts, and no-code automations. Fork it, brand it, host it.

Deploy on Hostinger

License: MIT CI Next.js 16 Supabase Stars

The marketing site and self-host docs live in a separate repo: ArnasDon/wacrm-site (wacrm.tech). This repo is the product — clone or fork it to run your own CRM.

What you get out of the box

  • Shared inbox on the official WhatsApp Business API — multiple agents working one number, per-conversation assignment, status, and notes.
  • Contacts + tags + custom fields, CSV import, deduplication.
  • Sales pipelines (Kanban) with deals linked to conversations.
  • Broadcasts with Meta-approved templates, delivery + read tracking, per-recipient variable substitution.
  • No-code automations — triggers on inbound messages, new contacts, keywords, or schedule; conditional branches, waits, tags, webhooks. Visual builder.
  • Real-time dashboard — response times, daily volume, pipeline value, cross-module activity feed.
  • Account management — email, password, avatar, global sign-out.

Why fork this?

This is a template, not a product. Forking means you get:

  • Full ownership — your code, your Supabase project, your domain, your data. No SaaS lock-in, no seat pricing, no trust dance.
  • Full customisation — add the fields your team needs, remove the modules you don't, redesign anything. The stack is boring on purpose (Next.js + Supabase + Tailwind) so the learning curve is short.
  • Zero ops to start — Hostinger Managed Node.js deploys a fork in a few clicks. No Docker, no Kubernetes, no infra team needed.
  • Real security primitives — token encryption (AES-256-GCM), RLS on every table, HMAC-verified webhooks, CSP, rate limiting, CI typecheck/build on every PR.

Not a framework. Not an SDK. A concrete, working CRM you can stand up in an afternoon and make yours.

Quick start

# Fork on GitHub first: https://github.com/ArnasDon/wacrm → Fork
git clone https://github.com/<your-username>/wacrm.git
cd wacrm
npm install
cp .env.local.example .env.local   # fill in Supabase + Meta creds
npm run dev

Open http://localhost:3000. You'll be redirected to /login (or /dashboard if already signed in).

Documentation

Full self-host documentation — Supabase migrations, WhatsApp Business API config, and production deploy — lives at wacrm.tech/docs (source: ArnasDon/wacrm-site).

Key pages:

Stack

  • App — Next.js 16 (App Router), React 19, TypeScript, Tailwind v4.
  • Data — Supabase (Postgres + Auth + Storage + RLS).
  • WhatsApp — Meta Cloud API (official WhatsApp Business API).

Contributing

This is a template, not a collaborative product — the expected flow is fork → customise → deploy, not upstream contribution. Bug reports and security issues are welcome; feature PRs often belong in your fork rather than here. Details in CONTRIBUTING.md and .github/SECURITY.md.

License

MIT. Fork it, brand it, host it.