Skip to Content
WhatsApp Agent Docs

Getting started

Prerequisites, the OpenWA gateway, environment variables, Neon setup, local development, linking a phone, and deploy.

Prerequisites

RequirementUsed for
Node.js 24 or newerWorkspace engines.node is >=24.
pnpm 12The repo pins pnpm@12.15.0 in packageManager.
A Neon Postgres databasePrimary store. DATABASE_URL must be a login in the app_runtime role (no BYPASSRLS).
Upstash QStashDurable job queue with deduplication and flow control.
Upstash RedisPer-tenant answer rate limit (REST URL and token).
Cloudflare accountWorkers for apps/engine and apps/admin, an R2 bucket named wa-blob, and a KV namespace bound as ROUTES.
DockerRuns the self-hosted OpenWA gateway (docker compose).
A spare WhatsApp numberLinked to the gateway. Use a dedicated number, never one you cannot afford to lose. See Ban-risk practices.
An Anthropic API keyAnswer generation.
A Resend account (optional)Handoff alert emails. Without RESEND_API_KEY the alerts are skipped.
A Sentry project (optional)Errors and traces. Without SENTRY_DSN the SDK is not initialized.

Install

pnpm install

pnpm-workspace.yaml lists the dependencies allowed to run build scripts (esbuild, lefthook, sharp, workerd). If pnpm blocks a new one, add it under allowBuilds.

Environment variables

Engine (apps/engine)

Secrets (set with wrangler secret put in production; locally all apps read the single root .env, copied from .env.example):

NamePurpose
DATABASE_URLNeon connection string.
ANTHROPIC_API_KEYClaude API key.
OPENWA_URLGateway base URL without /api (http://localhost:2785 locally).
OPENWA_API_KEYGateway admin key, see Run the gateway.
OPENWA_WEBHOOK_SECRETHMAC secret every session webhook is registered with. Verifies X-OpenWA-Signature.
OPENWA_WEBHOOK_URLOptional. Where the gateway delivers webhooks when it differs from PUBLIC_URL/webhooks/openwa. Local dev: http://host.docker.internal:8787/webhooks/openwa.
QSTASH_TOKEN, QSTASH_URLQStash credentials. QSTASH_URL is only needed for a regional endpoint.
QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEYVerify that job requests really come from QStash.
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKENRate limiter.
ENGINE_ADMIN_TOKENBearer token the admin app sends to /admin/*.
RESEND_API_KEYOptional. Handoff emails.
SARVAM_API_KEYOptional. Voice note transcription (Sarvam saaras:v4).
ELEVENLABS_API_KEYOptional. Voice note transcription (Scribe) and spoken voice-note replies.
SENTRY_DSNOptional. Enables Sentry; events are scrubbed of message content, phone numbers and tokens.
SENTRY_ENVIRONMENTOptional. Defaults to production.
SENTRY_TRACES_SAMPLE_RATEOptional. Defaults to 0.1. Claude spans record model, tokens and latency only.

Plain vars in apps/engine/wrangler.jsonc:

NameDefault in repoPurpose
EMAIL_FROMAssistant <assistant@example.com>Sender for alerts. Use a verified Resend domain.
ADMIN_URLhttps://admin.example.comUsed to build inbox links in emails.
PUBLIC_URLhttps://wa-engine.example.workers.devThe engine's public origin. QStash calls PUBLIC_URL/jobs/* and signature verification checks the same URL.

The R2 binding BLOB points at bucket wa-blob. The KV binding ROUTES caches the session to workspace route; create it with wrangler kv namespace create ROUTES and paste the id into wrangler.jsonc.

Admin (apps/admin)

Admin variables (Worker secrets and build env in production; the root .env locally):

NamePurpose
DATABASE_URLSame database as the engine.
BETTER_AUTH_SECRET, BETTER_AUTH_URLBetter Auth signing secret and the admin's own origin (http://localhost:3000 locally).
QSTASH_TOKEN, QSTASH_URLThe admin enqueues send and ingest jobs.
EMAIL_FROM, RESEND_API_KEYStaff and invitation emails.
ENGINE_URL, ENGINE_ADMIN_TOKENEngine origin and the shared bearer token.

Generate the tokens without printing them into shared logs:

openssl rand -hex 32      # ENGINE_ADMIN_TOKEN, OPENWA_WEBHOOK_SECRET

Run the gateway

The repo ships a docker-compose.yml that runs OpenWA v0.29.0 with the whatsapp-web.js engine. The gateway listens on 127.0.0.1:2785 only.

docker compose up -d openwa
docker compose exec openwa cat /app/data/.api-key   # copy into OPENWA_API_KEY

The compose file sets ENGINE_TYPE=whatsapp-web.js, MEDIA_DOWNLOAD_ENABLED=true (so media over the 1 MB inline limit can be fetched), mem_limit 2 GB (override with OPENWA_MEM_LIMIT; budget 300 to 500 MB per linked account), and SSRF_ALLOWED_HOSTS=host.docker.internal so the gateway can deliver webhooks to an engine running on your host. Session data lives in the openwa-data volume, so keep it: deleting it unlinks every account.

Unofficial gateway

OpenWA automates a real WhatsApp account. Read Ban-risk practices before linking a number.

Database

For local development run the setup script. It is idempotent and never prints connection strings:

./scripts/setup-neon-dev.sh

It uses the Neon project whatsapp-agent-openwa, creates the dev and integration-test branches (leaving main alone), runs both migrations (0000_init, 0001_security) as the owner role, creates the runtime login wa_dev (member of app_runtime, NOBYPASSRLS), and writes DATABASE_URL, MIGRATION_DATABASE_URL and TEST_DATABASE_URL into the root .env.

For any other database, set MIGRATION_DATABASE_URL to the owner role and:

pnpm db:generate   # only when you change packages/db/src/schema
pnpm db:migrate    # applies packages/db/migrations

Migration 0001_security forces row level security on every tenant table and creates the app_runtime role. Do not skip it: without it the Neon owner role bypasses the tenant policies. See Security.

Run locally

docker compose up -d openwa
pnpm --filter @wa/engine dev     # wrangler dev on :8787
pnpm --filter @wa/admin dev      # next dev on :3000
pnpm --filter @wa/docs dev       # this site

QStash must reach the engine over the public internet. For local end-to-end tests, expose wrangler dev through a tunnel and set PUBLIC_URL to the tunnel origin, otherwise job signature verification fails because the URL differs. OpenWA does not need the tunnel: in Docker it reaches the host at host.docker.internal:8787 (OPENWA_WEBHOOK_URL).

Sign in to the admin app, create a workspace, and open Connect WhatsApp. Scan the QR code from WhatsApp, Settings, Linked devices (or use a pairing code). Details in WhatsApp onboarding.

Simulate inbound messages

Without a phone, pnpm simulate posts signed OpenWA webhook payloads to a running engine. It reads OPENWA_WEBHOOK_SECRET and SIM_SESSION_ID from the root .env. Set SIM_SESSION_ID to the channel_connection.session_id of a connected workspace (or pass --session).

pnpm simulate text
pnpm simulate all --to http://localhost:8787

Scenarios: text, burst, image, voice, location, group, own, status, all. Options: --to, --session, --user (a <digits>@c.us id) and --text.

Deploy

Create the bucket and set secrets

wrangler r2 bucket create wa-blob
wrangler kv namespace create ROUTES   # paste the id into apps/engine/wrangler.jsonc
cd apps/engine
wrangler secret put DATABASE_URL
wrangler secret put ANTHROPIC_API_KEY
# repeat for every secret listed above

Deploy the engine

pnpm --filter @wa/engine deploy

Set PUBLIC_URL in wrangler.jsonc to the deployed origin first.

Point the gateway at the engine

In production run OpenWA on a host with Docker (a small VM works) and set OPENWA_URL to its address, keeping the port private or behind authentication. The engine registers each session's webhook automatically on connect, using OPENWA_WEBHOOK_URL or <PUBLIC_URL>/webhooks/openwa with OPENWA_WEBHOOK_SECRET. Use the engine's public https URL, so no SSRF_ALLOWED_HOSTS entry is needed.

Deploy the admin app

pnpm --filter @wa/admin deploy   # opennextjs-cloudflare build && deploy

Sign up in the admin app, create a workspace, then follow WhatsApp onboarding.

Build this documentation site

pnpm --filter @wa/docs build      # static export to apps/docs/out
pnpm --filter @wa/docs deploy     # wrangler deploy serves ./out as static assets

Set NEXT_PUBLIC_SITE_URL at build time to the public origin so the sitemap, robots file, canonical URLs, and Open Graph tags are correct.

On this page