TanStack Devtools is a modular system of packages organized into three layers: Framework Adapters, Core Shell, and Event Transport. This architecture lets you use pre-built devtools panels or build your own custom ones, regardless of which frontend framework you use.
Each framework adapter depends only on @tanstack/devtools. The core shell pulls in everything it needs, so end users install just two packages: their framework adapter and the Vite plugin.
The transport layer handles event delivery between plugins, the devtools UI, and (optionally) a dev server. It is composed of three pieces.
Runs inside the Vite dev server process (Node.js). It creates an HTTP server (or piggybacks on Vite's existing server when HTTPS is enabled) that accepts both WebSocket and SSE connections. When a message arrives from any client, the server broadcasts it to every other connected client and dispatches it on a server-side EventTarget so server-side listeners (like the Vite plugin's package-manager helpers) can react to it.
Key details:
Runs in the browser. Started automatically when the core shell mounts via TanStackDevtoolsCore.mount(). Its responsibilities:
The high-level, typed API that plugins use to send and receive events. Each EventClient is created with a pluginId and a type map that defines the events it can emit and listen to.
import { EventClient } from '@tanstack/devtools-event-client'
type MyEvents = {
'state-update': { count: number }
'reset': void
}
const client = new EventClient<MyEvents>({ pluginId: 'my-plugin' })
When you call client.emit('state-update', { count: 42 }), the EventClient:
When you call client.on('state-update', callback), the EventClient registers a listener on the global target for my-plugin:state-update events, so it receives events regardless of whether they came from a local emit or from the server bus.
The server bus is optional. Without the Vite plugin, EventClient still works for same-page communication via CustomEvent dispatch on window. Events simply won't cross tab or process boundaries.
The devtools shell is a Solid.js application that renders the entire devtools UI. It exposes the TanStackDevtoolsCore class with three methods:
The shell renders:
Settings and UI state (panel size, position, active tab, theme) are persisted in localStorage so they survive page reloads.
A shared Solid.js component library used by the core shell and available for use in Solid.js plugins. Provides buttons, inputs, checkboxes, a JSON tree viewer, section layouts, and other UI primitives. The @tanstack/devtools-utils package also depends on it to provide framework-specific plugin helpers.
A specialized EventClient pre-configured with pluginId: 'tanstack-devtools-core' and a fixed event map for devtools-internal operations:
This client is a singleton (devtoolsEventClient) used by both the core shell and the Vite plugin to coordinate.
Each framework adapter is a thin wrapper that bridges its framework's component model to the core Solid.js shell. The pattern is the same across all adapters:
The key insight: the core shell is always Solid.js, but your plugins run in your framework. A React plugin is a real React component rendered by React's createPortal into a DOM element that the Solid.js shell created. A Vue plugin is a real Vue component rendered by Vue's <Teleport>. The adapters bridge this gap so you never need to think about Solid.js unless you want to.
Adapters do not re-implement the devtools UI, manage settings, handle events, or communicate with the server. All of that lives in the core shell. Adapters are intentionally minimal -- typically a single file under 300 lines.
@tanstack/devtools-vite is a collection of Vite plugins that enhance the development experience and clean up production builds. It returns an array of Vite plugins, each handling a specific concern:
Uses Babel to parse JSX/TSX files and injects data-tsd-source attributes on every JSX element. These attributes encode the file path, line number, and column number of each element in source code, which the source inspector feature uses to implement click-to-open-in-editor.
Starts a ServerEventBus on the Vite dev server. Also sets up middleware for the go-to-source editor integration and bidirectional console piping (client logs appear in the terminal, server logs appear in the browser).
On production builds, transforms any file that imports from @tanstack/*-devtools to remove the devtools imports and JSX usage entirely. This means devtools add zero bytes to your production bundle.
Injects a small runtime into your application's entry file that intercepts console.log/warn/error/info/debug calls and forwards them to the Vite dev server via HTTP POST. The server then broadcasts them to connected SSE clients, enabling server-to-browser log forwarding.
Transforms console.* calls to prepend source location information (file, line, column), making it possible to click a console log and jump directly to the source.
Listens for install-devtools events from the devtools UI, runs the package manager to install the requested package, and then uses AST manipulation to inject the plugin import and configuration into the user's source code.
Replaces compile-time placeholders (__TANSTACK_DEVTOOLS_PORT__, __TANSTACK_DEVTOOLS_HOST__, __TANSTACK_DEVTOOLS_PROTOCOL__) in the event bus client code with the actual values from the running dev server, so the client automatically connects to the correct server.
To tie everything together, here is what happens when a plugin emits an event end-to-end:
Your library code calls eventClient.emit('state-update', data).
EventClient constructs a payload { type: 'my-plugin:state-update', payload: data, pluginId: 'my-plugin' } and dispatches it as a tanstack-dispatch-event CustomEvent on window.
ClientEventBus receives the tanstack-dispatch-event. It does three things:
If connected to the server bus, ClientEventBus also sends the event over WebSocket to ServerEventBus.
ServerEventBus receives the WebSocket message and broadcasts it to all other connected clients (WebSocket and SSE). It also dispatches the event on its server-side EventTarget so server-side listeners (e.g., the Vite plugin) can react.
In other browser tabs/windows, the event arrives via WebSocket from the server (or via BroadcastChannel from step 3). The local ClientEventBus dispatches it as a my-plugin:state-update CustomEvent, and any eventClient.on('state-update', callback) listeners fire with the data.
Without the Vite plugin and server bus, steps 4-6 are skipped, but steps 1-3 still work. This means plugins can communicate within a single page without any server infrastructure -- the server bus just adds cross-tab and cross-process capabilities.