Warning
Never use this tool in production.
pbrowser is built for local development and throwaway databases. It hands whoever is logged in full read/write access to any database they can reach, runs arbitrary SQL, and deletes rows in bulk. It has not been hardened or audited for production use.
If you deploy it against a production database anyway, you do so entirely at your own risk and take full responsibility for the consequences — including data loss, corruption, and credential exposure. The author provides no warranty and accepts no liability (see LICENSE).
A minimalist, self-hosted PostgreSQL browser and lightweight admin tool for the web. Login, paste a connection string, and you get a clean black-and-white UI to explore schemas, browse and edit rows, follow foreign keys, generate realistic random data, bulk-delete with checkboxes, edit the schema itself (tables, columns, indexes, constraints, views, functions, roles), export rows as CSV/JSON, and back up or restore the whole database — all without a single framework on the front end.
Built for developers who want pgAdmin-style power with the ergonomics of a static page.
- 🔐 Authentication built in — username/password from
.env, optional TOTP (RFC 6238) second factor. - 🔌 Per-session connection — users paste any Postgres URL after login; each session gets its own isolated pool.
- 💾 Saved connections — successful connections are remembered for one-click reconnect; the secret stays server-side, so only host/user/database metadata ever reaches the browser.
- 🗂️ Schema browser — schemas → tables → rows with pagination, sorting, and free-text filtering.
- ✏️ Full CRUD with type-aware inputs: enum columns become
<select>s, booleans become toggles, foreign keys become searchable pickers. - 🔗 Foreign-key navigation — outgoing FKs render as clickable links; every row exposes a "Related" view that pulls in incoming references.
- ⚡ Bulk random row generator — ~80 generators across 11 categories (names, addresses, dates, ids, lorem, etc.) with FK-valid picks, enum seeding, unique-constraint deduping, and exact-count retry.
- ☑️ Bulk delete with checkboxes — select rows across pages, transactional batched delete, supports composite primary keys.
- 🛠️ Schema editing (DDL) — create / rename / truncate / drop tables, add / alter / drop columns, reorder columns, and manage indexes and constraints from the Structure view — no hand-written SQL required.
- 🗄️ Database objects — browse and manage schemas, views & materialized views, functions / procedures, and roles from the Database view.
- ⭳ Row export — export the current selection, page, or the entire (optionally filtered) result set as CSV or JSON.
- 📦 Backup & restore — dump the whole database with
pg_dump(custom / plain / tar) and restore a dump withpg_restore/psql, straight from the browser. - 🗺️ Schema map — interactive ER-style diagram of every table, column, PK/FK tag, and relationship; drag, zoom, pan, and double-click a table to jump straight to its rows.
- 🧪 Raw SQL editor — Ctrl/Cmd+Enter to run, results rendered as a table.
- 🖤 Minimalist UI — vanilla JS, vanilla CSS, no build step.
All screenshots below were taken against a throwaway demo database seeded with synthetic data — no real records appear anywhere. Passwords and connection strings are blurred.
Username and password come from .env. When TOTP_SECRET is set, a third field
appears for the 6-digit code.
Successful connections are remembered for one-click reconnect; the connection string itself never leaves the server, so the cards only show host/user/database.
Paginated rows with sortable headers and free-text filtering. Foreign-key cells are clickable, and every row gets Related, Edit and Delete actions.
The editor is type-aware: enums render as <select>s, foreign keys as pickers,
nullable columns get a NULL toggle, and defaults are shown inline.
Related pulls every incoming reference to the current row, grouped by the table and column that points at it.
Each column gets an auto-detected generator you can override. FK columns are pre-loaded with valid targets, enums with valid labels, and unique constraints are listed so colliding rows can be skipped.
Selection persists across pages; the header checkbox supports an indeterminate state, and the delete runs as a transactional batch.
Arbitrary statements against the active connection, Ctrl/Cmd+Enter to run, results as a table.
The whole schema as an interactive diagram — PK/FK tags, bezier FK arrows, draggable nodes, zoom and pan.
Columns, indexes and constraints for the selected table, with inline DDL actions.
git clone https://github.com/iabdo9/pbrowser.git
cd pbrowser
cp .env.example .env
# edit .env — set AUTH_USER, AUTH_PASS, SESSION_SECRET
npm install
npm startOpen http://localhost:3000, login, then paste a Postgres connection string such as:
postgres://user:password@host:5432/dbname
npm run dev # node --watch server.jsAll configuration is read from .env at startup.
| Variable | Required | Default | Purpose |
|---|---|---|---|
AUTH_USER |
✅ | — | Login username |
AUTH_PASS |
✅ | — | Login password |
SESSION_SECRET |
✅¹ | random | Cookie session signing secret |
TOTP_SECRET |
— | Base32 TOTP secret. When set, a 6-digit code is required on login. | |
PORT |
3000 |
HTTP listen port | |
DEFAULT_PG_URL |
— | Pre-fills the connection-string input on the connect page | |
CONNECTIONS_FILE |
./connections.json |
Where saved connections are persisted. Holds DB passwords in plaintext, written 0600. |
|
EXPORT_MAX_ROWS |
200000 |
Row-export cap; larger result sets are truncated (the UI warns). | |
IMPORT_MAX_BYTES |
4 GiB |
Maximum size of an uploaded dump on the database Import. |
¹ A random secret is generated at startup if you omit SESSION_SECRET, but every restart will invalidate active sessions.
Generate a base32 secret, add it to your authenticator app (Aegis, 1Password, Google Authenticator, …) and to your .env:
node -e "console.log(require('otpauth').Secret.fromUTF8(require('crypto').randomBytes(20).toString('hex')).base32)"TOTP_SECRET=JBSWY3DPEHPK3PXP...When TOTP_SECRET is set, the login form shows a 6-digit code field.
- Click any table in the sidebar to load rows.
- Click a column header to sort; the filter box does a server-side
ILIKEacross text columns. - Use + Insert row to insert; the editor knows about enums, booleans, foreign keys, defaults, and nullability.
- Tables without a primary key are read-only in the row editor — use the SQL view to modify them.
- Cells whose column is an FK render as clickable links that jump to the referenced row.
- Each row exposes a Related button that lists all incoming references grouped by table.
Click ⚡ Generate rows on any table:
- Pick a count and an optional preset.
- Each column gets an auto-detected generator (e.g.
email,uuid_v4,date_past,fk_pick,enum); change any of them from a dropdown. - FK columns are pre-loaded with valid target values; enum columns are seeded with valid labels.
- Unique constraints (including Prisma-style
@@uniqueindexes) are detected; the generator retries until N unique rows are produced or combinations are exhausted. - Inserts run in transactional batches of 200.
- Tables with a primary key get a leftmost checkbox column.
- Tick rows individually or use the header checkbox to select all on the current page (supports an indeterminate state).
- Selection persists across pages, so you can accumulate rows and delete them at once.
- The 🗑 Delete N selected button runs a transactional delete in 500-row batches. Single-column PKs use
WHERE pk = ANY($1); composite PKs useWHERE (a, b) IN ((..), (..)).
The SQL view runs arbitrary statements against the active connection. Ctrl/Cmd+Enter to execute. Results are rendered as a table.
The Map view in the sidebar renders the entire current schema as an interactive diagram:
- Each table is a node with a dark title bar and a row per column showing the type and PK / FK tags.
- Foreign keys are drawn as bezier arrows from the child column to the referenced parent column (composite FKs included; hover an edge for a
from.col → to.coltooltip). - Drag a node by its title bar to rearrange; Relayout snaps everything back to an auto-grid.
- Pan by dragging the empty canvas, zoom with
Ctrl/Cmd + wheelor the+/−/100%buttons. - Double-click a node title to jump to that table's rows in the Tables view.
- Powered by a single
/api/schema-mapquery — no build step, no diagramming library, just SVG + DOM.
The Structure view manages the selected table without hand-written DDL:
- Table: create, rename, truncate, or drop.
- Columns: add, edit (name, type, default,
NOT NULL), or drop. The type field is a dropdown of common Postgres types. - Reorder columns: drag rows (or use ↑ / ↓) for a browser-local display order; Apply order to DB physically rewrites the table so the order is permanent.
- Indexes & constraints: list them with their definitions, create indexes (unique, method
btree/hash/gist/…), add or drop constraints.
The Database view manages objects beyond a single table for the connected database:
- Schemas — create, rename, drop.
- Views & materialized views — view the source, refresh a materialized view, create, or drop.
- Functions & procedures — inspect the source, create, or drop.
- Roles — list attributes (SUPERUSER, CREATEDB, LOGIN, …) and connection limits; create, edit, or drop.
Click ⭳ Export on any table to save rows as CSV or JSON:
- This page exports what's currently loaded, with no extra query.
- Selected rows exports the current cross-page checkbox selection (requires a primary key).
- All rows exports the entire result set, honoring the active filter and sort — capped at
EXPORT_MAX_ROWS(the UI warns if truncated).
The Database view's ⭳ Export / ⭱ Import buttons run PostgreSQL's client tools on the server:
- Export shells out to
pg_dump— pick the format (custom / plain SQL / tar) and contents (schema + data / schema only / data only). "Portable" adds--no-owner --no-privilegesso the dump restores cleanly into any role. The dump is prepared on the server, then streamed to your browser as a download. - Import auto-detects the dump format and restores it with
pg_restore(custom / tar) orpsql(.sql). Options: Clean first (drop existing objects before recreating) and Stop on first error. It's guarded by a type-IMPORT-to-confirm box, and the tool's stdout/stderr is shown when it finishes.
These two features require the
pg_dump,pg_restore, andpsqlbinaries to be present on the server host and to match the target server's major version. See Tech stack.
┌──────────────────────────┐ ┌───────────────────────────┐
│ Browser (vanilla JS) │ HTTP │ Express server │
│ public/login.html │ ─────▶ │ server.js │
│ public/app.html │ │ ├─ session (cookie) │
│ public/app.js │ │ ├─ per-session pg.Pool │
│ public/generators.js │ ◀───── │ ├─ pg (node-postgres) │
│ public/style.css │ │ └─ pg_dump / pg_restore │
└──────────────────────────┘ └─────────────┬─────────────┘
│ TCP
▼
PostgreSQL
- No build step. The frontend is plain ES modules and CSS served statically.
- Per-session pool. Each authenticated session owns a
pg.Pool, stored in aMap<sessionID, …>. Pools are closed on logout/disconnect. - SQL safety. All identifiers (schemas, tables, columns) are double-quoted; all values pass through parameterized queries. There is no string concatenation of user values into SQL.
Internal HTTP endpoints (all session-authenticated, JSON unless noted).
Auth & session
| Method | Path | Purpose |
|---|---|---|
| POST | /api/login |
Username/password (+ TOTP) login |
| POST | /api/logout |
Destroy session and pool |
| GET | /api/me |
Current session info |
| POST | /api/connect |
Open a pool — by raw connection string or saved id |
| POST | /api/disconnect |
Close the pool |
| GET | /api/connections |
List saved connections (metadata only, no secret) |
| PATCH | /api/connections/:id |
Rename a saved connection |
| DELETE | /api/connections/:id |
Remove a saved connection |
Browsing & data
| Method | Path | Purpose |
|---|---|---|
| GET | /api/schemas |
List schemas and tables |
| GET | /api/tables |
Tables in a schema |
| GET | /api/columns |
Columns, PK, FKs, unique constraints/indexes, enums |
| GET | /api/schema-map |
All tables, columns, and FK edges in a schema |
| GET | /api/rows |
Paginated rows with sort/filter |
| POST | /api/related |
Incoming references for a row |
| GET | /api/fk-values |
Foreign-key value picker |
| POST | /api/unique-tuples |
Existing tuples for unique-constraint deduping |
| POST | /api/export-rows |
Fetch rows (selection / filter) for CSV/JSON export |
Row mutations
| Method | Path | Purpose |
|---|---|---|
| POST | /api/insert |
Insert a single row |
| POST | /api/insert-bulk |
Transactional batched insert |
| POST | /api/update |
Update by PK |
| POST | /api/delete |
Delete a single row by PK |
| POST | /api/delete-bulk |
Transactional batched delete by PK list |
| POST | /api/query |
Run arbitrary SQL |
Structure (table DDL)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/indexes, /api/constraints |
Indexes / constraints for a table |
| POST | /api/ddl/table/{create,rename,drop,truncate,rebuild} |
Table-level DDL |
| POST | /api/ddl/column/{add,alter,drop} |
Column DDL |
| POST | /api/ddl/index/{create,drop} |
Index DDL |
| POST | /api/ddl/constraint/{add,drop} |
Constraint DDL |
Database objects
| Method | Path | Purpose |
|---|---|---|
| GET | /api/db/schemas |
Schemas with owners |
| POST | /api/ddl/schema/{create,rename,drop} |
Schema DDL |
| GET | /api/db/views |
Views & materialized views |
| POST | /api/ddl/view/{create,drop,refresh} |
View DDL |
| GET | /api/db/functions, /api/db/function-def |
Functions/procedures + source |
| POST | /api/ddl/function/{create,drop} |
Function DDL |
| GET | /api/db/roles |
Roles and their attributes |
| POST | /api/ddl/role/{create,alter,drop} |
Role DDL |
Backup & restore
| Method | Path | Purpose |
|---|---|---|
| POST | /api/db/export |
Run pg_dump, prepare a download |
| GET | /api/db/export/download/:id |
Stream the prepared dump, then delete it |
| POST | /api/db/import |
Restore an uploaded dump (pg_restore / psql) |
Not for production. See the warning at the top of this file — running pbrowser against a production database is unsupported and entirely at your own risk.
- Treat pbrowser as a privileged tool — anyone with login credentials can read and modify any database whose connection string they can supply, run arbitrary SQL, alter the schema, manage roles, and back up or restore the entire database. There is a single shared login; it is effectively a superuser console for whatever the connection can reach.
- Backup/restore spawns server-side processes. Export and import shell out to
pg_dump,pg_restore, andpsqlon the server host, and dumps are briefly written to a temp file before download. Anyone who can log in can trigger these. - Run it behind HTTPS and a reverse proxy. The server binds to all interfaces by default.
- Use a strong
SESSION_SECRET, enableTOTP_SECRET, and restrict network access (firewall, VPN, or proxybind). - Saved connections are stored in plaintext. Connection strings you save land in
connections.jsonnext to the server (override withCONNECTIONS_FILE), written0600and gitignored. They include database passwords. Delete the file — or skip saving connections — if that is not acceptable. Ad-hoc connections that you don't save are held only in server memory for the lifetime of the session.
- Server: Node.js 18+, Express 4,
pg8,express-session,otpauth,dotenv. - Client: Vanilla JS, no framework, no bundler.
- Database: Tested against PostgreSQL 13+.
- Optional CLI tools: the Backup & restore features shell out to
pg_dump,pg_restore, andpsql. They must be installed on the server host (e.g. Debian/Ubuntupostgresql-client) and their major version should match the target server. Everything else works without them.
- Export selection / query results as CSV / JSON.
- Per-connection bookmarks (saved connections).
- Full backup & restore (
pg_dump/pg_restore). - Saved queries.
- Dark mode.
PRs welcome — see Contributing.
- Fork and clone.
npm install.- Make your changes; run
node --check server.js && node --check public/app.jsto sanity-check syntax. - Open a pull request describing the change and how you tested it.
MIT — see LICENSE.









