---
id: tooling/devtools
title: Devtools
---

Stift ships as a *plugin* registered into the unified TanStack Devtools shell — the trigger button, dock, tabs, theme, and hotkeys are the shell's own; only the panel content is Stift's. Three tabs: **Overview** (status header, locale switch, namespace buckets, missing-key/param logs, pack state), **Config** (security, detection pipeline, loading strategy, locales — editable, saved to `stift.config.ts`), and **Namespaces** (buckets + cross-locale table with live editing).

## Install and mount

```bash
pnpm add @secundus-studio/stift-tanstack-devtool-plugin@0.4.0
```

```tsx
// devtools.tsx
import { useMemo } from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { createStiftDevtoolsPlugin } from '@secundus-studio/stift-tanstack-devtool-plugin/tanstack'
import { useStift } from '@secundus-studio/stift-react'

export function Devtools() {
  const Stift = useStift()
  const plugins = useMemo(() => {
    if (!import.meta.env.DEV) return [] // statically stripped in prod
    const [StiftDevtoolsPlugin] = createStiftDevtoolsPlugin({
      Stift,
      supportedLocales: Stift.config.locales.supported,
      allowFileWrites: true,
    })
    return [StiftDevtoolsPlugin()]
  }, [Stift])
  if (!import.meta.env.DEV) return null
  return <TanStackDevtools config={{ position: 'bottom-left', panelLocation: 'bottom' }} plugins={plugins} />
}
```

The `Stift` instance is created asynchronously at boot, so build the plugin from the provider context (`useStift()`) rather than a module-scope singleton. `import.meta.env.DEV` lets Vite's dead-code elimination drop the whole shell from production.

`createStiftDevtoolsPlugin` returns the documented TanStack factory pair: `Plugin()` mounts the panel content in a shell tab; `NoOpPlugin()` renders nothing, for prod builds that still build the plugins array.

## Panel actions

All via `StiftDevtools`, all surfacing errors through pack status + `hooks.onError`: locale switch, param/unit set+reset, pack install, wipe, stale refresh, single-ping update check (`checkAllForUpdates`), live-edit writes, and config writes (`writeConfigValue(['detection', 'order'], …)` → `stift.config.ts`, validated with rollback, applied on reload). Pack rows hide unless `pack.durability` is `'durable'` ([Offline & Persistence](../runtime/offline-persistence.md)).

## Dev-server bridge

The panel talks to the [vite plugin](./build.md)'s dev bridge (`/__stift_devtools/table`, `/write`) via the default `createViteDevtoolsTransport`. Writes only work when **both** `stiftPlugin({ devtools: { allowFileWrites: true } })` and the factory's `allowFileWrites: true` are set — otherwise the server never mounts the write route. In-process and TanStack EventClient transports are also available for non-Vite setups.

Editing a string in the panel persists through the `/write` bridge and lands **in place with no page refresh** (verified in the [Quickstart](../react/quickstart.md)).

## Next

- Pack lifecycle behind the panel → [Offline & Persistence](../runtime/offline-persistence.md)
- Missing-key/param logs → [Messages & Keys](../authoring/messages.md)
