@nice-code/devtools-relay
v0.58.0
Published
Readme
@nice-code/devtools-relay
Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms.txt — the complete, current docs flattened into plain text.
A tiny, reusable WebSocket relay that lets the nice-* devtools span browser
storage partitions — a private window, a different browser profile, a different
browser, even a different device — which same-origin BroadcastChannel cannot
cross (each partition is isolated by design, which is also why those environments
connect as a distinct client).
App clients and the devtools window each dial the relay; it rooms them by an app name and fans every frame out to the room's other members. It is purely ephemeral — no history, no content logging.
Runs on Bun (
Bun.serve) or Node (node:http+ws), picked at runtime bystartDevtoolsRelayAuto.
Let Vite run it for you
Usually you should not run this by hand at all. Add
@nice-code/devtools-vite
to your Vite config and the relay starts on the first devtools click, with every
local client discovering it — no port to configure, no second terminal:
import { niceDevtools } from "@nice-code/devtools-vite";
export default defineConfig({ plugins: [react(), niceDevtools()] });Or run it yourself
bunx @nice-code/devtools-relay # port 5199, binds 0.0.0.0
bunx @nice-code/devtools-relay --port 6000 --host 127.0.0.1Environment fallbacks: DEVTOOLS_RELAY_PORT, DEVTOOLS_RELAY_HOST.
Then point your app's relay URL at it — relayUrl on the devtools config, or
VITE_DEVTOOLS_RELAY_URL in the demos. An explicit URL bypasses discovery
entirely. The devtools transport connects with ?key=<app-name> so unrelated
apps on one relay never cross streams.
GET /health answers { "service": "nice-devtools-relay", "protocol": 1, "rooms": N }.
The service marker is what lets a prober tell a relay apart from anything else
that happens to hold the port — liveness is not identity.
Embed it
import { startDevtoolsRelayAuto } from "@nice-code/devtools-relay";
const relay = await startDevtoolsRelayAuto({ port: 5199 });
// …
relay.stop();startDevtoolsRelay (Bun) and startDevtoolsRelayNode (Node) are exported
directly when you know which host you want. Both reject with an EADDRINUSE
error — recognisable via isAddressInUseError — when the port is taken, which a
caller racing several dev servers onto one well-known port should treat as
"someone else's relay won" rather than a failure. probeRelay(url) answers
"relay" | "other" | "down".
Extend it
The room/fan-out logic lives in DevtoolsRelayHub, which is transport-agnostic
(it forwards opaque frames and knows nothing about WebSockets or the devtools
protocol). Wrap it in whatever carrier you need — the Bun server here, a Node
ws server, or a Cloudflare Durable Object for remote hosting:
import { DevtoolsRelayHub } from "@nice-code/devtools-relay";
const hub = new DevtoolsRelayHub();
hub.join(roomKey, conn); // conn: { send(data: string): void }
hub.relay(conn, frame); // → every other member of conn's room
hub.leave(conn);Security
It is an unauthenticated, ephemeral relay meant for local/dev use. The CLI
binds 0.0.0.0 by default so other devices on your LAN can reach it — pass
--host 127.0.0.1 to keep it same-machine only. (A relay started for you by
the Vite plugin binds loopback by default instead, on the grounds that a server
nobody consciously started should not be reachable from the network.) Do not
expose it to the public internet or point it at production clients without
adding auth and gating.
