Skip to main content
LiveStore is built with debuggability in mind. The DevTools give you a live window into your store’s state, event history, queries, sync status, and reactivity graph — all without adding instrumentation code.
DevTools are available to LiveStore sponsors. See the LiveStore GitHub repository for details.

DevTools overview

The DevTools panel includes the following features:

Data browser

Browse and edit your SQLite tables in real time with two-way sync. Changes made in the browser reflect in the app immediately.

Eventlog browser

See every event committed to the store in chronological order. Filter, search, and inspect individual event payloads.

Query inspector

View all active queries, their current results, and which signals they depend on.

Sync status

Monitor the connection to the sync backend. Toggle the sync latch to simulate offline behavior.

Signals graph

Visualize the dependency graph of computed signals and observe which queries trigger re-renders.

SQLite playground

Run arbitrary SQL queries against the live SQLite database directly from the DevTools panel.

Installing DevTools

Web (Vite)

Install the @livestore/devtools-vite package and add the plugin to your Vite config:
1

Install the package

npm install --save-dev @livestore/devtools-vite
2

Add the Vite plugin

vite.config.ts
import { defineConfig } from 'vite'
import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'

export default defineConfig({
  // ...
  plugins: [livestoreDevtoolsPlugin({ schemaPath: './src/livestore/schema.ts' })],
})
3

Open the DevTools

Run your app and navigate to localhost:3000/_livestore/web (or the URL logged in the browser console) to open the DevTools panel in a separate tab.

Chrome extension

You can also use the DevTools Chrome extension instead of the Vite-served panel.
1

Download the extension

Download the .zip matching your LiveStore version from the GitHub releases page. For example: livestore-devtools-chrome-0.3.0.zip.
2

Enable Developer mode

Navigate to chrome://extensions/ and enable Developer mode using the toggle in the top-right corner.
3

Load the extension

Click Load unpacked and select the unpacked folder, or drag and drop it onto the extensions page.
Install the extension version that matches the LiveStore version you are using. Mismatched versions may cause the DevTools to malfunction.

Expo (React Native)

Install the @livestore/devtools-expo package and configure your Metro bundler:
1

Install the package

npm install --save-dev @livestore/devtools-expo
2

Configure Metro

metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { addLiveStoreDevtoolsMiddleware } = require('@livestore/devtools-expo')

const config = getDefaultConfig(__dirname)

addLiveStoreDevtoolsMiddleware(config, { schemaPath: './src/livestore/schema.ts' })

module.exports = config
3

Open the DevTools

In the Expo CLI process, press Shift+m and select @livestore/devtools-expo. The DevTools panel opens in a new browser tab.

Node.js adapter

DevTools are configured automatically for makePersistedAdapter. The DevTools URL is logged when the app starts.
DevTools are not currently supported for makeInMemoryAdapter in the Node.js adapter.

Using the eventlog browser

The eventlog browser is the most useful tool for debugging unexpected state. What it shows:
  • Every event committed to the store, in order
  • Event name, timestamp, client ID, and full payload
  • Which materializer ran for each event
How to use it:
  1. Reproduce the bug or unexpected state in your app
  2. Open the eventlog browser
  3. Identify the event (or missing event) that caused the incorrect state
  4. Inspect the payload to confirm it contains the values you expected
Because LiveStore state is deterministic from the event log, if the state is wrong, either the wrong event was committed, the payload was incorrect, or the materializer has a bug.

Using the query inspector

The query inspector shows all currently active queryDb subscriptions and their live results. What it shows:
  • The SQL query string for each subscription
  • Current result set
  • Which signals the query depends on
  • Last time the query re-ran
How to use it:
  • If a component is not updating when you expect it to, check the query inspector to confirm the query is active and returning the expected rows
  • If a component is re-rendering too often, inspect the query’s signal dependencies to find unnecessary invalidations

Using the signals/reactivity graph

The signals graph visualizes computed signals and their dependency chains. Each node represents a signal or query; edges represent dependencies. Use the signals graph to:
  • Find computations that have unexpectedly large dependency sets
  • Trace why a specific computation re-ran after a commit
  • Identify signals that are recomputed more often than necessary

Debugging helpers on the store

The store object exposes a _dev property with helpers for programmatic debugging:
// Access internal diagnostics
store._dev

// Example: inspect pending events
console.log(store._dev)
The _dev API is intended for development and debugging use only. Do not rely on it in production code — it may change between versions.

exposeDebugUtils()

In development, you can expose the store and debug utilities on the global window object to inspect them from the browser console:
// In your store setup (development only)
if (import.meta.env.DEV) {
  exposeDebugUtils({ store })
}
This allows you to run queries, inspect state, and commit test events directly from the browser console without modifying your application code.

OpenTelemetry integration

LiveStore supports OpenTelemetry tracing via the otelOptions configuration. Traces capture store operations, event commits, materializer runs, and sync activity, giving you a detailed performance and correctness timeline.
1

Set up a tracer

otel.ts
import { ZoneContextManager } from '@opentelemetry/context-zone'
import { W3CTraceContextPropagator } from '@opentelemetry/core'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'

export const makeTracer = (serviceName: string) => {
  const url = import.meta.env.VITE_OTEL_EXPORTER_OTLP_ENDPOINT
  const provider = new WebTracerProvider({
    spanProcessors:
      url !== undefined
        ? [new SimpleSpanProcessor(new OTLPTraceExporter({ url: `${url}/v1/traces` }))]
        : [],
    resource: resourceFromAttributes({ 'service.name': serviceName }),
  })

  provider.register({
    contextManager: new ZoneContextManager(),
    propagator: new W3CTraceContextPropagator(),
  })

  return provider.getTracer('livestore')
}

export const tracer = makeTracer('my-app')
2

Pass the tracer to the worker

livestore.worker.ts
import { makeWorker } from '@livestore/adapter-web/worker'
import { tracer } from './otel.ts'
import { schema } from './schema.ts'

makeWorker({ schema, otelOptions: { tracer } })
3

Pass the tracer to the store

app.tsx
const useAppStore = () =>
  useStore({
    storeId: 'my-app',
    schema,
    adapter,
    batchUpdates,
    otelOptions: { tracer },
  })
Traces are exported to any OTLP-compatible backend (Jaeger, Honeycomb, Grafana Tempo, and so on). Set the VITE_OTEL_EXPORTER_OTLP_ENDPOINT environment variable to your collector URL.

Debugging sync issues

  1. Open the Sync status panel in DevTools and verify the connection is active.
  2. Check the Eventlog browser — confirm the events appear locally with the expected payloads.
  3. Check your sync backend logs for validatePayload errors or connection rejections.
  4. Verify your syncPayload auth token is valid and has not expired.
Because LiveStore state is deterministic from the event log, inconsistent state between clients means the event logs are diverging. Check:
  1. Whether all clients are connected to the same store ID
  2. Whether an unknownEventHandling strategy is silently dropping events on older clients
  3. Whether any materializer has non-deterministic behavior (random values, timestamps, side effects)
  1. Open the Query inspector and verify your query is active and returning updated rows
  2. Check the Signals graph for unexpected dependency chains that may be missing or over-broad
  3. Confirm you are using useQuery (or the equivalent for your framework) rather than reading state imperatively
Use the sync latch in the Sync status panel to close the sync connection without affecting the rest of the application. Commit events while offline, then re-enable the latch and verify events sync correctly.
  1. Reproduce the issue
  2. Open the Eventlog browser — check that the expected events were committed with correct payloads
  3. Open the Data browser — check the current state of the relevant SQLite tables
  4. If state is wrong given correct events, check the materializer for that event type
  5. If events are missing, check the application code that calls store.commit()
  6. If sync is wrong, check the Sync status panel and your backend logs

Build docs developers (and LLMs) love