Skip to content
Guides contents

GuidesTesting & troubleshooting

Devtools

Inspect mutations, live queries, and changes during development with Rindle devtools.

View as Markdown

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. The core also accepts a compatible { store, backend } composition; the available inspection details depend on its backend. They inspect state the client already holds. They do not add a data-tier connection and do not hold your database token. They must not ship to production.

Install

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

Apps scaffolded with create-rindle already include this wiring.

Attach the client

This example continues the manual quickstart. Append this block after its exported app in src/rindle-client.ts:

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:

// 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>
  );
}

Import DevTools from ./devtools.tsx and render <DevTools /> once in your application root.

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 targets 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:

import { app } from "./rindle-client.ts";
const { attachDevtools } = await import("@rindle/devtools");

const core = attachDevtools(app);
const unsubscribe = core.subscribe(() => {
  const { timeline, queries, deltas } = core.getState();
  console.log({ 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 RINDLE_DATABASE_TOKEN or other server 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