Inspecting Live Deployments
Connect a devtools window on your own machine to a deployed backend, whether a Durable Object or a long-lived server, 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 covers the deployed case: a staging or dev deployment you want to inspect from a window on your own machine. For example a misbehaving Durable Object, or a server whose traffic you want to read under real network conditions.
The mechanism is the one the local case uses: the window dials into the backend’s ordinary WebSocket endpoint, and devtools shares that connection as its own token-gated protocol on the wire mux. 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 all the code. 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:stagingWith no DEVTOOLS_TOKEN on the deployment, no devtools protocol is registered, so there is no route, no bridge and 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, from any app’s launcher or a bare mountDevtoolsWindow() page. Use the ⧉ backend control in the header, and 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 backend’s own clients dial. There is no separate dev route. Include whatever query params address the instance (
?matchId=…), exactly as a client would. - Off-machine URLs must be
wss://. The control refuses plainws://to a non-local host, with a visible reason. The devtools protocol travels over the link unencrypted (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 with a REMOTE badge, its live Server view and its per-client Traffic table. The wire client’s keep-alive ladder handles reconnection. A rewoken Durable Object re-attaches on its next link-up without waiting out the heartbeat.
To 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 ends up off, never open. That is what fail-closed means:
| Situation | Result |
|---|---|
No DEVTOOLS_TOKEN set on the deployment | Devtools protocol is never registered, so dials land in the unknown-prefix drop. |
stage: "production" (token set or not) | Host is inert by construction, so 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 uses 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). It never streams 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. While your window is attached, the Durable Object is kept awake, because 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. A deployed frontend’s browser clients are different: a window cannot dial into a browser, so they need a relay the producers dial out to, with authenticated short-lived sessions. That is the session relay. It is a distinct, more heavily gated feature than backend dial-in, covered in Remote Devtools Sessions.