dewee-webchat.php is a single-file backend for the chat widget (PHP >= 8.1, ext-openssl, no
Composer). It implements the HTTP contract and behaves like the Node
handler in dewee-webchat/server for anonymous visitors. The integration secret stays on your server.
No signed-in users. The PHP backend has no identity mode (it cannot sign partner assertions): every visitor is anonymous, with either transport. A visitor token that carries an identity session (issued by a Node backend with the same secret) is refused, never downgraded. For signed-in users, use the Node/Workers handler.
Copy dewee-webchat.php into your project and create an endpoint, for example api/dewee.php:
<?php
require __DIR__ . '/../lib/dewee-webchat.php';
DeweeWebchat::loadEnvFile(__DIR__ . '/../.env'); // optional; real env vars win
DeweeWebchat::handle(); // or DeweeWebchat::handle([...options])Point the widget at it: <script src="https://webchat-cdn.dewee.sh/dewee-webchat@latest/dewee-webchat.js" data-endpoint="/api/dewee.php" defer></script>.
Routes use the last path segment, so /api/dewee.php/chat (PATH_INFO) and a rewrite such as
/api/dewee/chat → api/dewee.php both work.
Same variables as optionsFromEnv() in the Node package. Create a Website chat integration in
CCP and copy its values (agents, allowed sites and limits are set there):
| Variable | Meaning |
|---|---|
DEWEE_CCP_CHAT_URL |
Ingress URL, https://<ccp>/api/public/webchat/v1/wci_… (https; plain http to localhost only) |
DEWEE_WEBCHAT_INTEGRATION_ID |
wci_…, must match the end of the URL |
DEWEE_WEBCHAT_INTEGRATION_SECRET |
dwc_…. Server only; sent to that origin only, redirects are refused |
DEWEE_WEBCHAT_SECRET |
HMAC secret for visitor tokens (>= 32 random chars; required with CCP) |
DEWEE_WEBCHAT_TRANSPORT |
Optional ccp or legacy-gateway; otherwise the variables set decide |
DEWEE_ALLOWED_ORIGINS (csv or *) |
Optional |
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY |
Require Cloudflare Turnstile for new sessions |
Deprecated legacy-gateway transport (direct Gateway URL and token, see
docs/migrating-to-ccp-transport.md):
| Variable | Meaning |
|---|---|
DEWEE_GATEWAY_URL / GOCLAW_GATEWAY_URL |
Gateway origin, e.g. https://gateway.example.com |
DEWEE_GATEWAY_TOKEN / GOCLAW_GATEWAY_TOKEN |
Gateway token or scoped operator API key |
DEWEE_AGENTS |
Comma-separated allowlist (first = default), or * |
DEWEE_DEFAULT_AGENT, DEWEE_TENANT_ID |
Optional |
DEWEE_ALLOW_CLIENT_CREDENTIALS=true, DEWEE_CLIENT_CREDENTIAL_HOSTS |
Bring-your-own gateway (playgrounds only) |
CCP and gateway variables together are refused: the endpoint answers 500 not_configured and logs
why (error_log), so a credential is never sent to an origin you did not choose. The deprecation
warning of the legacy transport is logged at most once a day.
With CCP, the integration config is cached for 60 s in a directory private to the PHP user,
sys_get_temp_dir()/dewee-webchat-<uid> (created 0700). If that directory is a link, belongs to
another user or is open to others (another account on a shared host could have created it), nothing
is cached and error_log says why; a cached file is only read when it is this user's and not
writable by others. The CCP edge session lives in the visitor token (encrypted, same format as the
Node handler), because PHP keeps no memory between requests. /session reports the edge session's
end as expiresAt (and expiresIn), so the widget asks for a new token before it lapses; that
refresh keeps the visitor and re-seals a refreshed edge session, and chat requests do not refresh or
open one. A returning visitor's /session is rate limited per visitor, a new visitor's per IP, and
with Turnstile only new visitors are challenged. CCP is told the visitor IP (X-Dewee-Visitor-IP)
only when it is a well-formed address; like the per-IP limits it comes from X-Forwarded-For, so
trust it only behind a proxy that overwrites that header. A turn that times out, or whose visitor disconnects (seen at the next
write, at most 15 s later), is aborted at CCP.
Array options use the TS names (transport, ccp = ['chatUrl' => …, 'integrationId' => …, 'integrationSecret' => …], gatewayUrl, token, agents, allowedOrigins, rateLimit =
false or callable(string $key, string $bucket): bool, onChat, timeoutMs, …). Rate limits
default to 20 messages/min per visitor+IP and 10 sessions/10 min per IP, kept in files under
sys_get_temp_dir(). They are defense in depth: the Dewee gateway owns the shared message limit, and its
POLICY_DENIED / RATE_LIMITED refusals (and, with transport ccp, CCP's refusals of a message) are answered as typed errors exactly like the TypeScript handler
(admission errors). Without a secret, a generated one is stored in the temp dir (set your own).
Each chat turn holds one PHP worker for the length of the reply (up to timeoutMs, default 180 s).
- Nginx + PHP-FPM: the response sends
X-Accel-Buffering: no; also setfastcgi_read_timeout 300s;and keepgzipoff fortext/event-stream. - Apache: use PHP-FPM with
flushpackets=on(ProxyPassMatch … flushpackets=on), or mod_php. Exclude the endpoint frommod_deflate(SetEnvIf Request_URI "^/api/dewee" no-gzip). - php.ini:
output_bufferingandzlib.output_compressionare turned off at runtime; if your host forbids that, disable them for the endpoint. - PHP built-in server: single-threaded, fine for local testing (
examples/php).