open-wa
DocsCloudflare Session Proxy

Cloudflare Session Proxy

Deploy the open-wa reverse tunnel on Cloudflare Workers and connect a local session without an inbound port.

Cloudflare Session Proxy

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 API

The 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 whoami

pnpm 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 deploy

Record 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 deploy

Run 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

SymptomCommand or observationExpected resultNext action
Worker deploy fails before creating a routepnpm exec wrangler whoami and inspect wrangler.jsoncThe 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 upstreamCheck the CLI log and pnpm exec wrangler secret listThe 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 ConsumerRepeat curl with the Authorization header aboveA matching consumer token reaches the Worker.Rotate CONSUMER_TOKEN if necessary and reconnect clients with the new value.
Consumer gets 503 Session offlineCheck the local CLI log for Connected upstream for session salesThe 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 504Call the same /api/... path against http://127.0.0.1:8080The 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.

On this page