A beautiful, local database client. Postico-inspired, runs in your browser. Supports PostgreSQL and MySQL, with SSH tunneling and a real SQL editor.
cd dbviewer
npm install
npm startOpen http://127.0.0.1:5435.
- PostgreSQL and MySQL — pick the driver per connection. The SQL editor auto-switches dialect.
- SSH tunneling — connect to databases behind a bastion, with private-key (passphrase optional) or password auth. Tunnel is opened lazily and torn down on disconnect.
- CodeMirror SQL editor — line numbers, syntax highlighting,
⌘↵to run, autocomplete (Ctrl-Space, auto-trigger after.) seeded with the live schema. - Query history — last 50 queries per connection persisted in SQLite; click an entry to load it back into the editor.
- macOS Keychain — opt-in per-connection password storage (
keytar→SecItemAdd). Default on for new connections. - Connections sidebar with environment color tags (dev / staging / prod / local).
- Schema tree — click a schema to expand, click a table to open it.
- Content tab — paginated rows (100 per page), striped grid, type badges in column headers.
- Structure tab — column list with type, nullability, default.
- SQL tab — transactional execution, query timing, 1,000-row safety cap.
- Read-only mode — wraps each SQL execution in
BEGIN READ ONLY(Postgres) /START TRANSACTION READ ONLY(MySQL). Recommended for prod.
- Localhost-only: server binds to
127.0.0.1(not0.0.0.0). - Host-header allowlist: rejects requests where
Hostisn'tlocalhost/127.0.0.1/[::1](DNS rebinding guard). - Cookie-based CSRF: every state-changing request must echo the CSRF cookie via
X-CSRF-Token. - No password persistence: connection metadata (host, user, db, SSH host/user/key path) lives at
~/.dbviewer/connections.jsonwith mode0600. Passwords and SSH passphrases are entered each session and held only in memory; they're discarded when the process exits. - Server-enforced read-only: queries run inside a transaction so writes can't slip through and errors roll back cleanly.
- Statement timeout: 30s per query (Postgres). Result row cap: 1,000.
- Identifier validation: the table-rows endpoint rejects schema/table names that aren't plain identifiers, so the only place raw SQL touches the wire is the SQL tab itself.
- CSP:
default-src 'self'with no remote scripts; CodeMirror is bundled locally and served from/cm.
When SSH is enabled on a connection:
- dbviewer opens an SSH client to your bastion.
- It starts a local TCP listener on
127.0.0.1:<random>. - Each driver connection through that listener is forwarded over the SSH session to the actual database host.
- On disconnect, the listener and SSH client are both torn down.
You can use either a private key (path; ~ is expanded) or a password. Key passphrase is asked at connect time.
dbviewer/
├── server.js Express server + API
├── lib/
│ ├── db.js SQLite (state.db) wrapper + schema migration
│ ├── store.js Connection metadata (CRUD on connections table)
│ ├── history.js Per-connection query history (last 50)
│ ├── pool.js Per-session driver/tunnel registry
│ ├── ssh.js ssh2 tunnel helper
│ ├── keychain.js macOS Keychain wrapper (keytar)
│ └── drivers/
│ ├── index.js Driver factory
│ ├── postgres.js pg driver implementation
│ └── mysql.js mysql2 driver implementation
├── public/
│ ├── index.html App shell (loads CodeMirror from /cm)
│ ├── styles.css Theme & components
│ └── app.js Frontend logic
└── package.json
Stored under ~/.dbviewer/ (mode 0700):
| File | Contents |
|---|---|
state.db |
SQLite — connections and query_history tables |
state.db-shm, state.db-wal |
SQLite WAL journal (auto-managed) |
connections.json.migrated-<ts> |
One-shot backup from the legacy JSON store on first SQLite boot |
Passwords are not in state.db — they live in the macOS Keychain (when "Save password to Keychain" is enabled on the connection) or in process memory only.
| Shortcut | Action |
|---|---|
⌘N / Ctrl+N |
New connection |
⌘↵ / Ctrl+Enter |
Run SQL query (when editor is focused) |
Esc |
Close modal |
| Variable | Default | Effect |
|---|---|---|
PORT |
5435 |
Port the server listens on |
- Result grid is non-virtualized; the 1,000-row cap keeps things responsive.
- MySQL
SHOW DATABASESis shown as the schema tree (system schemas filtered). - Statement timeout is enforced server-side for Postgres only; MySQL doesn't support per-pool
statement_timeout. UseSET SESSION MAX_EXECUTION_TIMEin a query if needed.
MIT — see LICENSE.