A Next.js reference application that demonstrates how tech providers can integrate with Meta's WhatsApp Business Platform. It covers the full lifecycle — onboarding businesses via Embedded Signup, managing WhatsApp Business Accounts, sending and receiving messages, and handling webhooks.
- Embedded Signup — Onboard SMBs with Facebook Login for Business (multiple ES versions supported)
- WABA Management — View and manage WhatsApp Business Accounts, phone numbers, and registration
- Messaging Inbox — Send and receive WhatsApp messages in real time via Ably WebSockets
- Template Messaging — Send pre-approved template messages (paid messaging)
- Webhook Viewer — Debug tool showing all incoming webhook payloads
- Asset Management — View shared Pages, Ad Accounts, Datasets, Catalogs, and Instagram Accounts
- Authentication — Secure login via Auth0
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) with React 19 |
| Language | TypeScript |
| Styling | Tailwind CSS 4 |
| Auth | Auth0 (@auth0/nextjs-auth0 v4) |
| Database | Vercel Postgres (Neon) |
| Real-time | Ably (WebSocket-based live updates) |
| Deployment | Vercel |
- Node.js 18.18+ (required by Next.js 15)
- An Ably account and app
- An Auth0 account and application
- A Meta Developer account and app
Click the button below to deploy to a new Vercel project. The flow will prompt you to fork the repo, connect a Neon Postgres database, and set environment variables.
Once deployed, open the Neon dashboard for your project and run the SQL in the Database Schema section below.
Follow the Configuration section to set up Ably, Auth0, and your Meta app.
# Clone the repository
git clone https://github.com/fbsamples/business-messaging-sample-tech-provider-app.git
cd business-messaging-sample-tech-provider-app
# Install dependencies
npm install
# Copy the example env file and fill in your values
cp .env.example .env.local
# Start the dev server (runs on https://localhost:3000)
npm run devThe dev server uses Turbopack with experimental HTTPS enabled (required by the Facebook JavaScript SDK).
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Production build |
npm run lint:all |
Run ESLint + TypeScript type-check |
npm run test |
Run unit tests (Vitest) |
npm run test:e2e |
Run end-to-end tests (Playwright) |
All required variables are listed below. Set them in .env.local for local development or in your Vercel project settings for production.
# Auth0 Configuration
# The Auth0 SDK reads these automatically — see https://auth0.com/docs/quickstart/webapp/nextjs
APP_BASE_URL='https://your-deployment-url.vercel.app'
AUTH0_DOMAIN='your-tenant.auth0.com'
AUTH0_CLIENT_ID='your-auth0-client-id'
AUTH0_CLIENT_SECRET='your-auth0-client-secret'
AUTH0_SECRET='a-long-random-string-for-session-encryption'
# Meta / Facebook Configuration
FB_APP_ID='your-facebook-app-id'
FB_APP_SECRET='your-facebook-app-secret'
FB_GRAPH_API_VERSION='v22.0'
FB_REG_PIN='your-registration-pin'
FB_VERIFY_TOKEN='your-webhook-verify-token'
# Ably Configuration
ABLY_KEY='your-ably-api-key'
# Contact
TP_CONTACT_EMAIL='your-contact-email@example.com'
# Database (auto-configured when using Vercel + Neon integration)
# POSTGRES_URL='postgres://...'| Variable | Description |
|---|---|
APP_BASE_URL |
Your deployment URL (e.g. https://your-app.vercel.app). Used by Auth0 for redirects. |
AUTH0_DOMAIN |
Your Auth0 tenant domain (e.g. your-tenant.auth0.com). |
AUTH0_CLIENT_ID |
Auth0 application client ID. |
AUTH0_CLIENT_SECRET |
Auth0 application client secret. |
AUTH0_SECRET |
A long random string used to encrypt Auth0 session cookies. Generate with openssl rand -hex 32. |
FB_APP_ID |
Your Meta app ID from the Meta Developer Dashboard. |
FB_APP_SECRET |
Your Meta app secret (App Settings > Basic). |
FB_GRAPH_API_VERSION |
Graph API version to use (e.g. v22.0). |
FB_REG_PIN |
A 6-digit PIN used when registering WhatsApp phone numbers. |
FB_VERIFY_TOKEN |
A secret string you choose — Meta sends it during webhook verification to prove it's you. |
ABLY_KEY |
API key from your Ably dashboard. |
TP_CONTACT_EMAIL |
Contact email displayed in the app. |
POSTGRES_URL |
PostgreSQL connection string. Auto-set when you connect Neon via Vercel. |
Ably provides real-time WebSocket connections for streaming incoming messages and webhook events to the browser.
- Create an account at ably.com
- Create a new app in the Ably dashboard
- Copy the API key(root) and set it as
ABLY_KEY
See the Ably documentation for more details.
Auth0 handles user authentication.
- Create an account at auth0.com
- Create a new Regular Web Application
- In the application settings, configure:
- Allowed Callback URLs:
https://your-app.vercel.app/auth/callback - Allowed Logout URLs:
https://your-app.vercel.app
- Allowed Callback URLs:
- Copy the Domain, Client ID, and Client Secret into your environment variables
See the Auth0 Next.js Quickstart for a full walkthrough.
- Create a Meta Developer account if you don't have one
- Create a new app and select the WhatsApp > Connect with customers through WhatsApp use case
- Create a Business Portfolio and connect it to the app
- In App Settings > Basic:
- Copy the App ID and App Secret to your env vars
- Add your Vercel deployment domain to App Domains
- In the Facebook Login for Business product settings:
- Add your deployment URL to Valid OAuth Redirect URIs (e.g.
https://your-app.vercel.app/) - Add your deployment URL to Allowed Domains for the JavaScript SDK
- Add your deployment URL to Valid OAuth Redirect URIs (e.g.
- In the WhatsApp product settings:
- Set the Callback URL to
https://your-app.vercel.app/api/webhooks - Set the Verify Token to the same value as your
FB_VERIFY_TOKENenv var - Subscribe to the
messageswebhook field
- Set the Callback URL to
- For development, add testers via App Roles — only users with a role on the app can use it in Development mode.
The steps above are sufficient for development and testing with app admins and testers. To make your app available to external businesses, complete the following:
- Replace the privacy policy — The app includes a placeholder privacy policy at
/privacy. Replace it with your own before submitting for app review. Set the URL in App Settings > Basic > Privacy Policy URL. - Create a Tech Provider Configuration — In the Meta Developer Dashboard, create at least one Tech Provider Configuration (config ID). The app fetches these dynamically and uses them to launch Embedded Signup.
- Complete Business Verification — Your business must be verified before your app can access production data from other businesses. See Business Verification.
- Submit for App Review — Request the permissions your app needs. See App Review. At minimum, request:
whatsapp_business_management— manage WABAs, phone numbers, templates, and webhook subscriptionswhatsapp_business_messaging— send text and template messages- Additional permissions based on your use case:
pages_show_list,catalog_management,ads_read
- Publish your App — Go to App Dashboard > Publish, click the publish button at the bottom. In Unpublished mode, only users with a role on the app (admin, developer, tester) can use it.
- Verify your webhook configuration — Confirm the callback URL (
https://your-domain/api/webhooks), verify token, andmessagesfield subscription are all set in WhatsApp > Configuration. - Verify your OAuth redirect URIs — Confirm your deployment URL is listed in Facebook Login for Business > Valid OAuth Redirect URIs and Allowed Domains for the JavaScript SDK.
After deploying, run the following SQL in your Neon dashboard (or any PostgreSQL client connected to your database) to create the required tables.
Each table stores data shared by businesses during the Embedded Signup flow. The user_id column scopes all data to the authenticated tech provider user.
CREATE TABLE IF NOT EXISTS wabas (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
waba_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_waba_key ON wabas (user_id, app_id, waba_id);
CREATE TABLE IF NOT EXISTS phones (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
phone_id BIGINT NOT NULL,
user_id VARCHAR,
is_ack_bot_enabled BOOLEAN DEFAULT FALSE,
ack_bot_message TEXT DEFAULT '',
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS phone_key ON phones (phone_id);
CREATE TABLE IF NOT EXISTS pages (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
page_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_page_key ON pages (user_id, app_id, page_id);
CREATE TABLE IF NOT EXISTS ad_accounts (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ad_account_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_ad_account_key ON ad_accounts (user_id, app_id, ad_account_id);
CREATE TABLE IF NOT EXISTS businesses (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
business_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_business_key ON businesses (user_id, app_id, business_id);
CREATE TABLE IF NOT EXISTS catalogs (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
catalog_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_catalog_key ON catalogs (user_id, app_id, catalog_id);
CREATE TABLE IF NOT EXISTS datasets (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
dataset_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_dataset_key ON datasets (user_id, app_id, dataset_id);
CREATE TABLE IF NOT EXISTS instagram_accounts (
key BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
instagram_account_id BIGINT NOT NULL,
user_id VARCHAR NOT NULL,
app_id BIGINT NOT NULL,
business_id BIGINT,
access_token TEXT,
last_updated TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS user_app_instagram_account_key ON instagram_accounts (user_id, app_id, instagram_account_id);app/
api/ # API routes (server-side)
beUtils.ts # Graph API wrappers, DB operations
authWrapper.ts # withAuth() HOF for protecting API routes
token/ # OAuth code-for-token exchange
send/ # Send WhatsApp messages
register/ # Register phone with WhatsApp
webhooks/ # Webhook listener (GET=verify, POST=incoming)
paid_messaging/ # Template message APIs
...
components/ # React components
types/ # Shared TypeScript type definitions
publicConfig.ts # Non-secret config (app ID, API version)
privateConfig.ts # Secret config (app secret, tokens — server-only)
my-inbox/ # Messaging inbox page
my-wabas/ # WABA management page
paid_messaging/ # Template messaging page
...
lib/
auth0.ts # Auth0 client initialization
middleware.ts # Auth0 middleware (protects all routes)
See CONTRIBUTING.md for guidelines on code style, security practices, and how to submit changes.
This project is MIT licensed, as found in the LICENSE file.
Terms of Use — https://opensource.facebook.com/legal/terms
Privacy Policy — https://opensource.facebook.com/legal/privacy