Getting started
Prerequisites, the OpenWA gateway, environment variables, Neon setup, local development, linking a phone, and deploy.
Prerequisites
| Requirement | Used for |
|---|---|
| Node.js 24 or newer | Workspace engines.node is >=24. |
| pnpm 12 | The repo pins pnpm@12.15.0 in packageManager. |
| A Neon Postgres database | Primary store. DATABASE_URL must be a login in the app_runtime role (no BYPASSRLS). |
| Upstash QStash | Durable job queue with deduplication and flow control. |
| Upstash Redis | Per-tenant answer rate limit (REST URL and token). |
| Cloudflare account | Workers for apps/engine and apps/admin, an R2 bucket named wa-blob, and a KV namespace bound as ROUTES. |
| Docker | Runs the self-hosted OpenWA gateway (docker compose). |
| A spare WhatsApp number | Linked to the gateway. Use a dedicated number, never one you cannot afford to lose. See Ban-risk practices. |
| An Anthropic API key | Answer 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 installpnpm-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):
| Name | Purpose |
|---|---|
DATABASE_URL | Neon connection string. |
ANTHROPIC_API_KEY | Claude API key. |
OPENWA_URL | Gateway base URL without /api (http://localhost:2785 locally). |
OPENWA_API_KEY | Gateway admin key, see Run the gateway. |
OPENWA_WEBHOOK_SECRET | HMAC secret every session webhook is registered with. Verifies X-OpenWA-Signature. |
OPENWA_WEBHOOK_URL | Optional. 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_URL | QStash credentials. QSTASH_URL is only needed for a regional endpoint. |
QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEY | Verify that job requests really come from QStash. |
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN | Rate limiter. |
ENGINE_ADMIN_TOKEN | Bearer token the admin app sends to /admin/*. |
RESEND_API_KEY | Optional. Handoff emails. |
SARVAM_API_KEY | Optional. Voice note transcription (Sarvam saaras:v4). |
ELEVENLABS_API_KEY | Optional. Voice note transcription (Scribe) and spoken voice-note replies. |
SENTRY_DSN | Optional. Enables Sentry; events are scrubbed of message content, phone numbers and tokens. |
SENTRY_ENVIRONMENT | Optional. Defaults to production. |
SENTRY_TRACES_SAMPLE_RATE | Optional. Defaults to 0.1. Claude spans record model, tokens and latency only. |
Plain vars in apps/engine/wrangler.jsonc:
| Name | Default in repo | Purpose |
|---|---|---|
EMAIL_FROM | Assistant <assistant@example.com> | Sender for alerts. Use a verified Resend domain. |
ADMIN_URL | https://admin.example.com | Used to build inbox links in emails. |
PUBLIC_URL | https://wa-engine.example.workers.dev | The 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):
| Name | Purpose |
|---|---|
DATABASE_URL | Same database as the engine. |
BETTER_AUTH_SECRET, BETTER_AUTH_URL | Better Auth signing secret and the admin's own origin (http://localhost:3000 locally). |
QSTASH_TOKEN, QSTASH_URL | The admin enqueues send and ingest jobs. |
EMAIL_FROM, RESEND_API_KEY | Staff and invitation emails. |
ENGINE_URL, ENGINE_ADMIN_TOKEN | Engine origin and the shared bearer token. |
Generate the tokens without printing them into shared logs:
openssl rand -hex 32 # ENGINE_ADMIN_TOKEN, OPENWA_WEBHOOK_SECRETRun 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_KEYThe 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.shIt 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/migrationsMigration 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 siteQStash 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).
Link a phone
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:8787Scenarios: 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 aboveDeploy the engine
pnpm --filter @wa/engine deploySet 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 && deployCreate a workspace and link a number
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 assetsSet NEXT_PUBLIC_SITE_URL at build time to the public origin so the sitemap, robots file, canonical URLs, and Open Graph tags are correct.