Skip to content

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.

Nothing new to write — the createNiceDurableObjectDevtools / createNiceServerDevtools call you already have is the whole surface. 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

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

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

Every way this can be misconfigured resolves to off, never open:

SituationResult
No DEVTOOLS_TOKEN set on the deploymentDevtools 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 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 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. 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) — never 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 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.

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.