CLI database ops, as buttons. A web UI over the database CLI commands you
keep half-remembering. No AI, no magic — just psql operations turned
into clicks, running fully on your machine.
⚠️ Local-only by design. cli2ui has no authentication and binds to your machine. Run it on localhost or inside a trusted network — never expose it to the public internet. See SECURITY.md.
psql -c "SELECT * FROM pg_stat_activity" → one "running queries" button
pg_dump -t users mydb → one "back up this table" button
SELECT pg_terminate_backend(pid) → one "kill this process" button
Built for app developers and solo developers — not DBAs. The pitch is "you're looking at your tables 3 minutes after deciding to" — no install marathon, no connection-wizard maze, no digging through nested trees. And no "now go find a demo database" homework, either: a sample database is bundled and pre-wired into the connection form, with a realistic Airlines dataset one opt-in away.
The UI ships in English and Japanese — a header toggle switches between them, and it auto-detects your browser's language on first visit.
| Table detail | SQL runner |
|---|---|
![]() |
![]() |
docker compose upThen open http://localhost:8000. The connection form is pre-filled to point at a bundled sample database — just hit Connect and you'll see its tables.
To point at your own PostgreSQL, change the form fields (host, port, db, user, password) and connect.
Port 8000 already taken? It's a common default, so a clash is likely. Just remap the host side in
docker-compose.yml—"8000:8000"→"8001:8000"(any free port works) — and open that port instead. Only the number on the left changes; the app keeps listening on 8000 inside the container.
The bundled shop database is deliberately tiny. For something with real
volume — enough rows for the health checks, activity views, and jsonb tooling
to have something to chew on — there's an opt-in second database: the
Postgres Pro "Airlines" demo dataset
(flights and bookings, ~250MB restored, jsonb columns included).
seed/demo/fetch.sh # downloads the dump (~60MB, not committed)
docker compose --profile demo up -d airlines # first boot loads it — takes a few minutesThen connect with host airlines, database demo, user demo, password demo
(port 5434 if connecting from your host machine). The loaded data persists in
a named volume, so the slow load only happens once. fetch.sh also accepts
small or big if you want a different dataset size.
cli2ui runs in its own container, so "localhost" in the connection form means the cli2ui container, not your machine. Your database almost always lives somewhere else — another container, another compose project, or a native install.
New to Docker networking? Don't guess — README.NETWORKING.md walks through why
localhostfails and gives a copy-paste fix for every setup (native DB, another compose project, a published port), with diagrams and a full worked example.
The short version — three ways to reach it (no code changes, just how you fill the form):
-
Via the host (simplest). If the database publishes a port on your machine (e.g.
-p 5432:5432), set the connection host tohost.docker.internaland the port to the published one. Works the same on macOS, Windows, and Linux — theextra_hostsentry indocker-compose.ymlmakes the name resolve everywhere. -
Share a network, connect by container name. Put both on one external network and use the database's container name as the host:
docker network create cli2ui-net # then in BOTH compose files: # networks: # default: # name: cli2ui-net # external: true
-
Attach at runtime. Join the running cli2ui container to the database's existing network, then use the DB container name as the host:
docker network connect <db-network> <cli2ui-app-container>
The Replication panel reads pg_stat_replication / pg_replication_slots, so
its Standbys table stays empty until a replica is actually attached. To
watch it populate, spin up a throwaway primary + standby pair and point cli2ui
at the primary — see README.REPLICATION.md for a
copy-paste compose file and the exact connection details.
v1.2.0 — a multi-DB (PostgreSQL & MySQL) ops console. docker compose up →
connect → browse your tables in a DB-client layout (table list in the sidebar,
table detail in the main pane).
- ✅ PostgreSQL: connect + list tables (estimated row counts)
- ✅ Table detail: column definitions with column & table comments and
generated-column expressions (
\d+ table), an on-demand JSON shape view onjson/jsonbcolumns (sampled top-level keys, value types, nesting depth, GIN coverage — describes the rows examined, not every row), a row preview, and a filter builder — stack column / operator / value rows (ANDed) into a read-onlySELECT … WHERE, no SQL typing - ✅ SQL runner: read-only ad-hoc queries by default (
SET TRANSACTION READ ONLYstatement_timeout+ 1000-row cap), with an opt-in write mode that commits — guarded by a whole-database safety snapshot taken before each write
- ✅ Export to CSV / JSON: download a full query result set (past the display cap) or an entire table's rows, streamed through a server-side cursor (UTF-8 BOM on CSV for Excel)
- ✅ CSV import: append rows from a CSV into an existing table (
COPY, matched by header name) in one all-or-nothing transaction, with a safety snapshot first - ✅ EXPLAIN snapshots + diff: save query plans and diff two (before/after an index) instead of copy-pasting plans into a scratch file
- ✅ Scale simulation: EXPLAIN the same query at 1× / 100× / 10000× the real row
counts (a what-if
pg_classedit, always rolled back) to see where the plan shape breaks as data grows - ✅ Index lab: measure a hypothetical index (
EXPLAIN ANALYZEbefore/after, the index created and rolled back in one transaction) before you commit to creating it - ✅ Activity: running queries + connections from
pg_stat_activity, with one-click cancel (pg_cancel_backend) / kill (pg_terminate_backend) - ✅ Objects browser: databases (
\l), schemas (\dn), roles (\du) — a read-only listing, with create / rename / alter / drop (and DB clone viaTEMPLATE) behind confirm gates in a side drawer - ✅
postgresql.confeditor: read/edit parameters viapg_settings+ALTER SYSTEM SET+pg_reload_conf(), with reload-vs-restart badges - ✅ Locks: the wait-for chain folded into a tree — the head blocker at the
top (cancel / kill it to free the whole chain beneath) with the blocked
sessions nested under it (
pg_locks+pg_blocking_pids), blocking cycles detected and flagged, one-click cancel / kill on any session - ✅ Replication: readiness check (
wal_level/max_wal_senders) + WAL position, connected standbys (pg_stat_replication), and slot create / drop - ✅ Health: table sizes, unused indexes, unindexed foreign keys (FK columns
with no index the planner can use for them — Postgres doesn't auto-create one,
and a partial or invalid index doesn't count), redundant indexes
(a non-unique index that's a leading prefix of, or identical to, another),
orphan rows (foreign keys left
NOT VALID, plus*_idcolumns with no FK that name-match a table's primary key — each with an on-demand, read-only anti-join count that never validates or changes anything), dead-rows / vacuum, and a stats-only bloat estimate — read-only catalog facts, no advice - ✅ Dependencies: the foreign-key graph topologically sorted (
graphlib) into a safe TRUNCATE/DELETE order and its reverse (the load order), with foreign-key cycles detected and named — read-only, nothing is truncated (PostgreSQL & MySQL) - ✅ Extensions: what's installed in this database and what the server could
install (the Web equivalent of
\dxunioned withpg_available_extensions), with an update-available badge when an installed version lags the server's default — read-only;CREATE EXTENSIONstays in the SQL runner - ✅ Command history: SQL run through the runner, logged to the management DB — status, row count, timing, and one-click re-open
- ✅ Backup / restore: automatic table snapshots (
pg_dumpcustom format) before every destructive or structural change (drop / truncate / rename / alter), kept under a per-connection size budget (oldest pruned automatically — tune withCLI2UI_MAX_AUTO_BACKUP_BYTESper snapshot andCLI2UI_MAX_AUTO_BACKUP_TOTAL_BYTESin total), plus restore of an uploaded dump — streamed to the client tool (not buffered in memory) — into a new database or, with a type-gate, an existing one - ✅ Workspace overview: a bento dashboard summarizing every panel (live counts for activity, snapshots, backups, replication readiness…), one click away
- ✅ Internationalization: full English / Japanese UI with a header toggle
(cookie-persisted, falling back to the browser's
Accept-Language) - ✅ MySQL (8.0+): the same console over a MySQL connection — browse, read-only +
write query runner, filter builder, CSV import, CSV/JSON export,
EXPLAIN(into the shared plan tree, so snapshots/diff work), index + table + column DDL, database create/drop, activity (SHOW PROCESSLIST), locks (performance_schema), health (unused indexes / sizes), backup & restore (mysqldump/mysql), a settings editor (SET PERSIST), and replication status. Features with no MySQL equivalent (vacuum/bloat, schemas separate from databases, the planner what-if lab, replication slots) show as "not applicable" rather than an empty card.
| Layer | Choice | Why |
|---|---|---|
| Frontend | htmx + Alpine.js | Click → swap in HTML. No SPA build needed. |
| Backend | Django | Battle-tested psycopg2; management plumbing for free. |
| Management DB | SQLite | Saved connections + (later) history. Zero extra infra. |
| Infra | Docker | Local-only, self-contained. No DB creds ever leave your network. |
- No AI. API key management + sending your schema to a third party is a non-starter for the people this is for.
- No SaaS. The moment we'd hold your DB connection info, the liability and encryption story swamps the project. Local-only keeps it honest.
cli2ui is local-only by design — no SaaS, no auth, no outbound calls — so the
threat model is narrow: it runs next to your database on your machine. Even so,
the destructive surface is taken seriously: identifiers are bound with
psycopg2.sql.Identifier, the SQL runner defaults to SET TRANSACTION READ ONLY,
what-if features always ROLLBACK, CSRF is enforced, and DEBUG is off by
default. The full threat model and static-analysis results live in
specs/security-check.md. To report a vulnerability,
see SECURITY.md.
pip install -r requirements.txt
python manage.py migrate
python manage.py runserverYou'll need a PostgreSQL to connect to (the sampledb service in
docker-compose.yml is one option, exposed on localhost:5433).
DEBUG is off by default so error pages don't leak tracebacks, settings, or
SQL. Set DJANGO_DEBUG=1 while developing if you want Django's rich error pages.
Issues and PRs welcome. See CONTRIBUTING.md for how to run the app, the test suite, and the UI conventions (STYLE.md) new panels follow.
MIT © TABATA Hitoshi


