Inspecting Live Deployments
Dial a deployed backend — a Durable Object or a long-lived server — from a devtools window on your own machine, over an encrypted, token-gated link that a production build refuses by construction.
Observing Backends gets a backend into the window on localhost and wrangler dev. This page is the deployed case: a staging or dev deployment you want to look at from a window running on your own machine — a misbehaving Durable Object, a server whose traffic you want to read against real network conditions.
The mechanism is the same one the local case already uses — the window dials into the backend’s ordinary WebSocket endpoint, where devtools rides the wire mux as its own token-gated protocol. Only two things change for a deployment: the URL is remote (so it must be wss://), and the token lives in your deploy config instead of a .env file.
Enable it on the backend
Section titled “Enable it on the backend”Nothing new to write — the createNiceDurableObjectDevtools / createNiceServerDevtools call you already have is the whole surface. What changes is deploy configuration:
const devtools = createNiceDurableObjectDevtools(ctx, { runtime, stage: env.DEVTOOLS_ENV ?? "development", // declare it: "staging", not the dev default token: env.DEVTOOLS_TOKEN, // a wrangler secret — unset ⇒ inert realms: { match: () => this.engine },});On the deployment, set the two values out of band so neither lands in source:
# a strong random secret — the window must present exactly this to be admittedwrangler secret put DEVTOOLS_TOKEN# declare the stage explicitly; do NOT lean on the "development" default on a deploywrangler deploy --var DEVTOOLS_ENV:stagingNo DEVTOOLS_TOKEN on the deployment ⇒ no devtools protocol is registered ⇒ no route, no bridge, no cost. Enabling inspection is opt-in per deployment, and it is off until you set that secret.
Dial it from a window on your machine
Section titled “Dial it from a window on your machine”Open a devtools window locally (any app’s launcher, or a bare mountDevtoolsWindow() page) and use the ⧉ backend control in the header: paste the backend’s WebSocket URL and its token.
wss://api.demo.example.com/match/ws?matchId=42- The URL is the same endpoint the game clients dial — no separate dev route. Include whatever query params address the instance (
?matchId=…), exactly as a client would. - Off-machine URLs must be
wss://. A plainws://to a non-local host is refused in the control with a visible reason: the devtools protocol rides the link plain (the token is the gate, not encryption), so TLS is what protects it in transit. - The token never enters any app bundle — it lives only in the window page you typed it into. Recently-dialed URLs are remembered in the window’s own storage for one-click reconnect; tokens are not persisted.
A dialed backend appears in the switcher as an ordinary backend client, wearing a REMOTE badge, with its live Server view and per-client Traffic table. Reconnection is the wire client’s keep-alive ladder; a rewoken Durable Object re-attaches on its next link-up without waiting out the heartbeat.
If you set the endpoint at build time instead of dialing by hand, the niceDevtools() Vite plugin’s backends option takes the same { url, token } — point url at the wss:// deployment.
The fail-closed matrix
Section titled “The fail-closed matrix”Every way this can be misconfigured resolves to off, never open:
| Situation | Result |
|---|---|
No DEVTOOLS_TOKEN set on the deployment | Devtools protocol never registered — dials land in the unknown-prefix drop. |
stage: "production" (token set or not) | Host is inert by construction — admission is never served. |
| Wrong token from the window | auth_denied; the window warns; nothing is streamed. |
Plain ws:// to a remote host | Refused in the connect control before any socket opens. |
What streams, and what does not
Section titled “What streams, and what does not”The remote link inherits the same data-minimization defaults as any server scope:
- Traffic is sizes and counts, never payload bytes — the wire lane records frame kinds and byte counts only.
- Action payloads do not leave the process by default.
actionDomainScopestreams action id, domain, status, timings and error shape;input/outputvalues stay put unless you passlogPayloads: truedeliberately. - The backend realm scope is metrics-only.
realmEngineScopestreams connection counts, ingress/egress frames/bytes/messages, the coalesce ratio and a per-connection table (named by each connection’s avatar) — never the values in the realm tree. Realm contents only stream from a frontend producer, and there they are gated behindstreamContents— see Remote Devtools Sessions.
Hibernation, on a deployment
Section titled “Hibernation, on a deployment”The hibernation caveat is unchanged and now literal: while your window is attached, the Durable Object is kept awake (the heartbeat re-wakes an evicted DO). On a metered deployment that is real cost for as long as you watch. Detached, it costs nothing — the bridge arms zero timers, and the DO hibernates cleanly. Close the window when you are done.
Inspecting deployed frontends
Section titled “Inspecting deployed frontends”Dialing in covers backends. Getting a deployed frontend’s browser clients into a window — where the window cannot dial into a browser — needs a relay the producers dial out to, with authenticated short-lived sessions. That is the session relay — a distinct, more heavily-gated surface than backend dial-in — covered in Remote Devtools Sessions.