# Devtools

Inspect a Rindle app in development: mutation timeline, live query inspector, raw delta stream, and the dev-only wiring for @rindle/devtools and @rindle/react-devtools.

Rindle's in-browser devtools are split into two packages:

- **`@rindle/devtools`** - a framework-agnostic core that attaches to a running
  `createRindleClient` app and builds a read-only inspection model.
- **`@rindle/react-devtools`** - the floating React panel over that core.

Use both in development. They inspect state the client already holds; they do
not add a daemon connection, do not hold your daemon token, and should not ship
to production.

## Install

```bash
pnpm add -D @rindle/devtools @rindle/react-devtools
# or npm i -D @rindle/devtools @rindle/react-devtools
```

Apps scaffolded with [`create-rindle`](/docs/create-rindle) already include this
wiring.

## Attach the client

After the browser client is ready, attach it behind a static dev flag:

```ts
// src/rindle-client.ts
const app = await createRindleClient({
  schema,
  mutators,
  api: { url: "" },
  daemon: { wsUrl: "ws://127.0.0.1:7601" },
});

if (import.meta.env.DEV) {
  void import("@rindle/devtools").then(({ attachDevtools }) => attachDevtools(app));
}
```

The dynamic import matters: in a production Vite build, `import.meta.env.DEV` is
statically false, so the devtools core is dropped from the production bundle.

## Mount the React panel

Mount the panel once near the root. In an SSR app, mount it only after the first
client effect so the server markup and hydration markup match:

```tsx
// src/devtools.tsx
import { lazy, Suspense, useEffect, useState } from "react";

const Panel = import.meta.env.DEV
  ? lazy(() => import("@rindle/react-devtools").then((m) => ({ default: m.RindleDevtools })))
  : null;

export function DevTools() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!Panel || !mounted) return null;

  return (
    <Suspense fallback={null}>
      <Panel />
    </Suspense>
  );
}
```

Then render `<DevTools />` beside your app provider:

```tsx
<RindleApp>
  <App />
</RindleApp>
<DevTools />
```

The panel auto-discovers the most recently attached client through the devtools
hub. It starts as a small Rindle launcher in the corner of the page.

## What it shows

- **Mutation timeline** - each optimistic mutation from invoke to pending to
  confirmed or dropped, including snap-back highlights when the server result
  diverges from the prediction.
- **Queries inspector** - every live materialized view, its result type, row
  count, AST, sample rows, and whether pending mutations touch its tables.
- **Delta stream** - the raw incremental change tape for each query: `Add`,
  `Remove`, `Edit`, and nested child deltas.

This is aimed at the optimistic loop: what did the browser predict, what did the
authority accept, and how did the live views rebase?

## Core-only use

The core has no DOM assumptions. If you are not using React, attach the client
and render the state yourself:

```ts
const { attachDevtools } = await import("@rindle/devtools");

const core = attachDevtools(app);
const unsubscribe = core.subscribe(() => {
  const { timeline, queries, deltas } = core.getState();
  renderDevtools({ timeline, queries, deltas });
});

// Later:
unsubscribe();
core.detach();
```

`@rindle/react-devtools` re-exports the core helpers for convenience, but the data
model lives in `@rindle/devtools`.

## Production rules

Keep both packages dev-only and behind static dev gates. The recommended pattern
is:

- dynamically import `@rindle/devtools` only after `createRindleClient` resolves;
- dynamically import `@rindle/react-devtools` only when `import.meta.env.DEV`;
- in SSR apps, render no panel on the server or hydration pass;
- never pass daemon credentials to devtools.

The client exposes small read-only inspection hooks for the core. Nothing runs
unless you import the package and call `attachDevtools`.

## Next steps

- [The browser client](/docs/client) - where `createRindleClient` is configured.
- [Server rendering](/docs/ssr) - how to keep the panel client-only in an SSR app.
- [Scaffold with create-rindle](/docs/create-rindle) - a starter with devtools
  already wired.

---

[View this page on Rindle](https://rindle.sh/docs/devtools)
