Skip to content

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.

Nothing new to write. The createNiceDurableObjectDevtools / createNiceServerDevtools call you already have is all the code. What changes is deploy configuration:

MatchDO.ts
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:

Terminal window
# a strong random secret — the window must present exactly this to be admitted
wrangler secret put DEVTOOLS_TOKEN
# declare the stage explicitly; do NOT lean on the "development" default on a deploy
wrangler deploy --var DEVTOOLS_ENV:staging

With 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.

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 plain ws:// 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.

Every way this can be misconfigured ends up off, never open. That is what fail-closed means:

SituationResult
No DEVTOOLS_TOKEN set on the deploymentDevtools 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 windowauth_denied; the window warns; nothing is streamed.
Plain ws:// to a remote hostRefused in the connect control before any socket opens.

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. actionDomainScope streams action id, domain, status, timings and error shape; input/output values stay put unless you pass logPayloads: true deliberately.
  • The backend realm scope is metrics-only. realmEngineScope streams 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 behind streamContents. See Remote Devtools Sessions.

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.

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.