Cloudflare Session Proxy
Deploy the open-wa reverse tunnel on Cloudflare Workers and connect a local session without an inbound port.
The Cloudflare Session Proxy lets a local Easy API session make an outbound WebSocket connection to a Cloudflare Worker. Remote consumers call the Worker, so the local machine needs no inbound port. The Worker is still a remotely reachable authenticated entry point, and it must be protected with its consumer token and any Easy API key.
Topology and limits
Remote consumer --HTTPS/WebSocket--> Cloudflare Worker --Durable Object--> local TunnelClient --HTTP--> Easy APIThe Worker authenticates /sessions/:sessionId/upstream with UPSTREAM_TOKEN and all other session paths with CONSUMER_TOKEN. It routes each session ID to one Durable Object. HTTP proxy requests wait at most 30 seconds; an absent upstream returns 503 Session offline, and a timeout returns 504 Gateway Timeout from session upstream.
The Durable Object uses Cloudflare's WebSocket hibernation APIs and an edge ping/pong response, which can reduce active compute while the connection is idle. It does not make the total deployment free: Worker requests, Durable Objects, storage, egress, domains, and account-plan charges remain subject to current Cloudflare billing. Check Cloudflare Workers pricing for the account and plan you use.
Deploy from an empty directory
@open-wa/cf-proxy is currently a private monorepo package, so it is not installed from npm. A fresh deployment starts with the repository checkout that contains the Worker entrypoint.
git clone https://github.com/open-wa/wa-automate-nodejs.git
cd wa-automate-nodejs
corepack enable
pnpm install
cd packages/cf-proxy
pnpm exec wrangler login
pnpm exec wrangler whoamipnpm exec wrangler whoami must show the Cloudflare account that owns the Worker and its Durable Object namespace. This checkout already contains src/index.ts, src/tunnel-do.ts, and wrangler.jsonc; do not cd into a path that does not exist in a standalone npm consumer.
The checked-in wrangler.jsonc is the binding and migration required by the entrypoint:
{
"name": "open-wa-proxy",
"main": "src/index.ts",
"compatibility_date": "2024-03-20",
"durable_objects": {
"bindings": [{
"name": "SESSION_TUNNEL",
"class_name": "SessionTunnel"
}]
},
"migrations": [{
"tag": "v1",
"new_classes": ["SessionTunnel"]
}]
}Set both Worker secrets without putting their values in the repository or shell history:
export OPENWA_PROXY_UPSTREAM_TOKEN="$(openssl rand -hex 32)"
export OPENWA_PROXY_CONSUMER_TOKEN="$(openssl rand -hex 32)"
printf '%s' "$OPENWA_PROXY_UPSTREAM_TOKEN" | pnpm exec wrangler secret put UPSTREAM_TOKEN
printf '%s' "$OPENWA_PROXY_CONSUMER_TOKEN" | pnpm exec wrangler secret put CONSUMER_TOKEN
pnpm exec wrangler deployRecord the deployed workers.dev hostname as OPENWA_PROXY_HOST, for example https://open-wa-proxy.<account>.workers.dev. Secrets are independent: the local session needs the upstream token, while remote callers need the consumer token.
Deploy from an existing checkout
Contributors who already have the repository use the same entrypoint and binding from the repository root:
pnpm install
pnpm --filter @open-wa/cf-proxy exec wrangler whoami
pnpm --filter @open-wa/cf-proxy exec wrangler deployRun secret put again only when rotating a value. The Worker code reads the secrets by the exact names UPSTREAM_TOKEN and CONSUMER_TOKEN; renaming them requires a runtime change.
Attach a local session
Run the CLI on the machine that owns the WhatsApp browser session. It keeps the upstream WebSocket open and reconnects after a disconnect:
export OPENWA_PROXY_HOST='https://open-wa-proxy.<account>.workers.dev'
npx @open-wa/wa-automate@5.1.0 \
--session-id sales \
--host 127.0.0.1 \
--port 8080 \
--proxy-host "$OPENWA_PROXY_HOST" \
--proxy-token "$OPENWA_PROXY_UPSTREAM_TOKEN"The CLI converts the HTTPS host to a WebSocket host and connects to /sessions/sales/upstream. The Worker does not run WhatsApp or the browser; those stay on the local machine and still determine session readiness and Easy API behavior.
Consume the session
Use the consumer token in Authorization: Bearer rather than a URL query whenever possible, because query strings are more likely to appear in access logs:
curl -i \
"${OPENWA_PROXY_HOST}/sessions/sales/api/getHostNumber" \
-H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN"For an Easy API command with a body, include the local API key too when the session was started with --api-key:
curl -i -X POST \
"${OPENWA_PROXY_HOST}/sessions/sales/api/sendText" \
-H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN" \
-H "X-API-Key: $OPENWA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"args":["1234567890@c.us","Hello through Cloudflare"]}'The Worker forwards the request to the local Easy API through the upstream connection. A remote caller with the consumer token but no required Easy API key still receives the local API's authentication response.
Node clients can use the compatibility URL, which selects the tunnel transport in @open-wa/socket-client:
import { SocketClient } from '@open-wa/socket-client';
const client = await SocketClient.connect(
`cf-proxy://open-wa-proxy.<account>.workers.dev?sessionId=sales&token=${encodeURIComponent(process.env.OPENWA_PROXY_CONSUMER_TOKEN)}`,
);
console.log(await client.getHostNumber());Custom domain
Keep workers.dev until the Worker works end to end. When a Cloudflare zone is ready, add a Worker custom domain or route in the Cloudflare dashboard or Wrangler configuration, deploy again, and replace OPENWA_PROXY_HOST with the resulting https:// hostname. The local session and remote consumers must use the same Worker hostname; a DNS record pointing at another Worker will produce a valid-looking but incorrect endpoint.
Troubleshooting
| Symptom | Command or observation | Expected result | Next action |
|---|---|---|---|
| Worker deploy fails before creating a route | pnpm exec wrangler whoami and inspect wrangler.jsonc | The account is authenticated and SESSION_TUNNEL binds SessionTunnel. | Log in to the intended account and deploy from packages/cf-proxy, or fix the binding/migration file. |
| Local CLI reports unauthorized upstream | Check the CLI log and pnpm exec wrangler secret list | The CLI log should say it connected upstream; the secret list contains UPSTREAM_TOKEN. | Set that secret to the exact value passed to --proxy-token, then restart the CLI. |
Consumer gets 401 Unauthorized Consumer | Repeat curl with the Authorization header above | A matching consumer token reaches the Worker. | Rotate CONSUMER_TOKEN if necessary and reconnect clients with the new value. |
Consumer gets 503 Session offline | Check the local CLI log for Connected upstream for session sales | The upstream WebSocket is open for the same session ID. | Start the local CLI, match --session-id to the URL path, and resolve local session readiness. |
Consumer gets 504 | Call the same /api/... path against http://127.0.0.1:8080 | The local request completes within 30 seconds. | Fix the local API, browser, or Easy API key before investigating Cloudflare routing. |
Was this helpful?
Your answer includes the page path and docs version.
