Skip to content

Browser SQLite Explorer#520

Draft
raivieiraadriano92 wants to merge 15 commits intomainfrom
raivieiraadriano92/db-explorer
Draft

Browser SQLite Explorer#520
raivieiraadriano92 wants to merge 15 commits intomainfrom
raivieiraadriano92/db-explorer

Conversation

@raivieiraadriano92
Copy link
Copy Markdown
Collaborator

@raivieiraadriano92 raivieiraadriano92 commented Mar 29, 2026

Landscape Research

Research date: 2026-03-29

Existing Solutions

NPM Packages (embeddable)

Solution Type Fits our use case?
Drizzle Studio Embeddable (@drizzle-team/studio) Web component, 210K downloads No — commercial license, designed for server-side DBs, not in-browser SQLite
PGlite REPL (@electric-sql/pglite-repl) React component No — Postgres only. But architecturally the closest pattern to what we'd want
Expo Drizzle Studio Plugin Expo DevTools plugin No — React Native/Expo only
LiveStore Devtools npm package No — tightly coupled to LiveStore framework

Browser Extensions

Extension What it does Limitation
OPFS Explorer (Chrome) Browse OPFS file tree File browser only — can't query data
OPFS Viewer (Chrome) Same idea Same limitation
SQLite Viewer (Chrome) Opens SQLite files from OPFS Mixed reviews, can't connect to a live in-memory DB
SQLite Explorer (Chrome) DevTools panel for sql.js sql.js specific, unclear wa-sqlite support

Standalone Web Apps

Project Stars Tech Reusable?
sqlime.org 1,100 Vanilla JS, sql.js No — standalone app
browser-sqlite-editor New React, wa-sqlite, CodeMirror 6, TanStack Table, React Flow (ERD!) No — standalone PWA, not published as package
sqlite-online 139 React, sql.js No — standalone app
sqliteviewer.app WASM-based, very polished No — standalone app
PowerSync Diagnostics App Standalone web app No — separate auth, not embeddable

Key Finding

There is a clear gap: no open-source, embeddable React component exists for exploring in-browser SQLite databases (wa-sqlite, sql.js, PowerSync). Every existing solution is either:

  • A standalone app (not embeddable)
  • Commercial (Drizzle Studio)
  • Wrong database engine (PGlite = Postgres)
  • A browser extension that can't access the live JS database instance

npm Package vs Browser Extension

npm package is the clear winner. Here's why:

  1. Live database access required — a browser extension communicates via message passing and cannot access WASM memory or JS objects directly. The adapter pattern requires a JS reference to the DB.

  2. Established pattern — React Query DevTools, TanStack Router DevTools, and LiveStore Devtools all ship as npm packages with a React component. This is the standard React ecosystem approach for dev tooling.

  3. Distribution model:

    • Ship as @thunderbolt/sqlite-explorer (or similar)
    • Provide two modes: floating (small toggle button that expands to overlay) and embeddable (render as a regular component)
    • Accept a SqliteExplorerAdapter as prop (existing pattern)
    • Auto-exclude from production via process.env.NODE_ENV
    • Tree-shakeable
  4. Adapter pattern already solves the hard problemSqliteExplorerAdapter is DB-agnostic. Users can implement it for wa-sqlite, sql.js, PowerSync, SQLocal, or any other browser SQLite. Ship a few built-in adapters (createDrizzleExplorerAdapter, createSqlJsAdapter, etc.)

  5. The browser-sqlite-editor project (React + wa-sqlite + CodeMirror 6) validates the tech stack but is a standalone PWA — extracting it as a component would be significant work. We're already building the component-first version.

Market Opportunity

The local-first / offline-first ecosystem (PowerSync, wa-sqlite, sql.js, SQLocal, Electric SQL) is growing fast and has zero embeddable debugging tools. Publishing this as an npm package following the TanStack DevTools pattern would fill a genuine unserved niche.

Sources

raivieiraadriano92 and others added 12 commits March 29, 2026 11:10
- define SqliteExplorerAdapter contract for DB-agnostic exploration
- add Drizzle adapter implementation using sqlite_master and PRAGMA introspection
- include component state types (DbExplorerState, DbExplorerAction)
- add useDbExplorerState hook with reducer for table browsing, pagination, and custom SQL execution
- add useColumnResize hook for interactive column width adjustment via mouse drag
- display tables and views grouped with icons and row counts
- highlight selected object and support click-to-select
- Add SqlEditor component with CodeMirror 6, SQLite dialect, and dark mode support
- Install codemirror, @codemirror/lang-sql, @codemirror/state, @codemirror/view, and @codemirror/theme-one-dark
- Support Cmd+Enter to execute queries, display execution time and errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resizable columns via useColumnResize hook
- Click-to-copy cell values with visual feedback
- Pagination controls with configurable page sizes
- NULL value styling and empty state handling
- add DbExplorerPage with page header and adapter wiring
- add DbExplorer component with resizable sidebar, SQL editor, and data table
- register /db-explorer route in app.tsx
- adapter now supports both array and object row formats since
  PowerSync returns objects while wa-sqlite returns arrays
- filter out internal ps_ prefixed tables from the object list
- add debug logging for object loading
- the ps_ exclusion was too broad and hid PowerSync data tables
  that users need to inspect in the db-explorer
- show all tables and views in sqlite_master without filtering
- display indexes and triggers alongside tables and views
- group objects by type with distinct icons
- only fetch row counts for tables and views
- extract DbObjectType and use config-driven grouping to reduce duplication
- Fetch sqlDefinition and tblName from sqlite_master for all objects
- Show parent table name for indexes/triggers in the object list
- Display PRAGMA index_info with CREATE statement for indexes
- Display CREATE TRIGGER definition for triggers
- Remove default SELECT * query when selecting non-table objects
- groups start collapsed by default to reduce visual noise
- add chevron indicator with rotate animation on expand
- show item count badge in group header
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 29, 2026

PR Metrics

Metric Value
Lines changed (prod code) +1050 / -0
JS bundle size (gzipped) 1.09 MB (no baseline yet — merge to main first)
Test coverage 71.57% (no baseline yet — merge to main first)
Load time (preview) Lighthouse results unavailable

Updated Sun, 29 Mar 2026 16:54:26 GMT · run #330

… SQL

- display sqlDefinition directly in the result grid instead of querying sqlite_master
- eliminates unnecessary database round-trip for trigger inspection
… or execution

- Add dedicated SqlDefinition component with CodeMirror read-only editor
- Introduce viewMode state to switch between query and definition views
- Unify index and trigger selection into a single code path
@raivieiraadriano92 raivieiraadriano92 changed the title Raivieiraadriano92/db explorer Browser SQLite Explorer — Landscape Research Mar 29, 2026
@raivieiraadriano92 raivieiraadriano92 changed the title Browser SQLite Explorer — Landscape Research Browser SQLite Explorer Mar 29, 2026
- rename SCREAMING_CASE constants to camelCase per project conventions
- add braces to single-line if/return statements
- remove stale console.log in object loading
- fix long lines and improve formatting consistency
@cjroth cjroth temporarily deployed to raivieiraadriano92/db-explorer - thunderbolt PR #520 March 29, 2026 16:52 — with Render Destroyed
Copy link
Copy Markdown
Member

@cjroth cjroth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this - I think we should either find a way to make this only bundle in dev builds or maybe even put it into a browser extension (btw is there a major benefit to using this vs an existing sqlite browser extension?)

If we are going to go forward with it, I'd want to add tests.

Here's Claude's code review:


Overview

A new devtools page that lets developers explore the in-browser SQLite database. It features:

  • A collapsible sidebar listing tables, views, indexes, and triggers
  • A CodeMirror-based SQL editor with Cmd+Enter execution
  • A paginated data table with resizable columns and click-to-copy
  • An adapter pattern (SqliteExplorerAdapter) making the component DB-agnostic
  • Read-only SQL definition view for indexes/triggers

The architecture is solid — clean adapter pattern, proper use of useReducer for complex state, and
state/logic extracted into a custom hook per project conventions. Well-researched PR description too.


Issues

  1. SQL Injection via unescaped identifiers in use-db-explorer-state.ts

The adapter has a proper escapeId() helper, but the state hook constructs SQL with raw template
literals:

// use-db-explorer-state.ts:152
adapter.execute(SELECT * FROM "${name}" LIMIT ${defaultPageSize} OFFSET 0)

// use-db-explorer-state.ts:206
adapter.execute(SELECT * FROM "${state.selectedObject}" LIMIT ${state.pageSize} OFFSET ${offset})

A table named foo"bar would break the query. Either expose escapeId from the adapter or move these
queries inside the adapter itself (e.g., adapter.getPage(name, limit, offset)).

  1. Duplicated useIsDarkMode hook

Identical function in both sql-definition.tsx:13-18 and sql-editor.tsx:18-23. Extract to a shared
file.

Also, when theme === 'system', this reads window.matchMedia at render time but doesn't subscribe to
changes. Per project conventions, useSyncExternalStore would be the right fit here, since this is
exactly "subscribing to a browser API."

  1. Sequential row count loading

object-list.tsx:29-39 fetches row counts in a serial for loop:

for (const obj of countable) {
counts.set(obj.name, await adapter.getRowCount(obj.name))
}

With many tables, this is noticeably slow. Promise.all (or batched parallel) would be faster.

  1. columns stored in state but never read

state.columns is populated in SELECT_OBJECT (use-db-explorer-state.ts:77) but no component ever reads
it. The DataTable uses result.columns from the query result instead. Dead state — remove it or use
it.

  1. resetWidths defined but unused

use-column-resize.ts:37 exports resetWidths, which is never called. Remove it.

  1. Custom SQL runs without pagination

runQuery (use-db-explorer-state.ts:175-192) sets totalRows: result.rows.length, meaning all rows are
fetched into memory. For a SELECT * FROM on a large table, this could be expensive. Consider wrapping
custom queries in a COUNT(*) subquery or at least adding a default LIMIT.

  1. No production guard

The PR description mentions "auto-exclude from production via process.env.NODE_ENV", but the route in
app.tsx:115 is unconditional. This will ship to production. Should be gated like the tasks route:

{isDev && <Route path="db-explorer" element={} />}

  1. No tests

No test files included. The drizzle-adapter.ts (SQL parsing, row normalization, column extraction)
and use-db-explorer-state.ts (reducer logic) are both very testable and have meaningful edge cases
worth covering.


Minor / Style

  • data-table.tsx:66 — (row as unknown[]): The QueryResult type already declares rows: unknown[][], so
    this cast shouldn't be needed. If it is, the type might not be flowing right.
  • Double border on sidebar: db-explorer.tsx:29 has border-r on the sidebar container, but the
    ResizableHandle already provides a visual separator.
  • setPageSize async pattern (use-db-explorer-state.ts:221-243): The inner run() function is defined
    and immediately called. An async IIFE or extracting to a separate function would be cleaner.
  • Hardcoded Cmd+Enter in sql-editor.tsx:108: On Windows/Linux this would be Ctrl+Enter. The keymap
    binding uses Mod-Enter correctly, but the displayed hint says Cmd+Enter.

What's good

  • The adapter interface is well-designed and genuinely DB-agnostic
  • useReducer + extracted state hook follows project conventions
  • CodeMirror integration is clean — callback refs avoid stale closures, theme changes handled
    properly
  • Column resize hook is a nice touch
  • The temp view approach for extracting column names from arbitrary queries is clever
  • PR description with landscape research is thorough

Verdict

Good foundation for a dev tool. The main things to address before merging: production guard on the
route, extract/fix useIsDarkMode, escape identifiers consistently, and remove dead code (columns
state, resetWidths). Tests would be nice but less critical for an experimental devtool behind a
feature gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experiment Testing an idea out - not necessarily going to get merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants