The control tower for your Postgres fleet.
pgtower.dev · formerly pgtui — what changed
A keyboard-first PostgreSQL administration TUI for sysadmins — think k9s, but for Postgres. A control tower sees every aircraft, spots conflicts before they happen and decides who goes next; pgtower does that for your Postgres servers. Where most database TUIs are data browsers, pgtower leans into operations: watch and kill sessions, manage roles and grants, create/drop databases, and inspect cluster health — all with strong guards against destructive mistakes.
Single static binary, no dependencies, no container required.
Recorded with VHS against throwaway clusters —
regenerate with vhs docs/demo/demo.tape.
Tools like pgcli, lazysql and rainfrog are great for browsing data and
running queries. pgtower overlaps there (it has a query runner and a read-only
data browser), but its focus is cluster administration:
- Sessions — see every client backend and
pg_cancel_backend/pg_terminate_backenda runaway one. - Roles — create users, edit attributes, grant and revoke privileges on databases, and drop roles (including a safe force-drop that reassigns ownership instead of deleting data).
- Databases — create and drop databases, browse tables and sizes, and get a
\d-style structure view. - Locks — a blocking tree showing who is waiting on whom.
- Dashboard — connections vs
max_connections, cache hit ratio, uptime, replication, longest active query, plus a connection advisor that flags when you're near the limit or drowning in idle connections. - Tuning — a read-only settings advisor, an
ALTER SYSTEMeditor, and apg_hbaviewer/editor with a lockout-proof safety net. - Servers — keep several clusters in one place and hop between them with
S; test reachability, tag production servers, and get a plain-language diagnosis when one can't be reached.
Core management (roles, databases, sessions, queries) works without a
superuser — a role with CREATEROLE/CREATEDB is enough. The Tuning
tab's ALTER SYSTEM and pg_hba editing are the exception and require a
superuser (see Permissions).
| Tab | What it does |
|---|---|
Servers (S) |
Connection manager: list, switch, add / edit / delete, t test (latency + server version), * set the default. Tag servers dev / staging / prod (prod gets a red header badge). Connection failures are explained — not responding, refused, no route, auth failed, pg_hba rejected, TLS … — with what to check next, instead of a raw driver error. |
| 1 · Dashboard | Cluster health: connections vs max_connections (with reserved slots), cache hit ratio, uptime, total size, commits/rollbacks, version, longest active query, replication. A connection advisor flags near-limit/idle-dominated/idle-in-transaction situations and tells you what to do. Auto-refresh. |
| 2 · Databases | Databases (owner, size, connections) → tables → read-only data browser (horizontal column scroll ←→, per-column search /, top query bar e). Create (n) / drop (D) databases, d for a table's structure (columns, indexes, constraints), and / for a fuzzy quick-find in the database/table list. |
| 3 · Query | SQL editor with a paged result grid. x runs EXPLAIN (plan only). Writes require confirmation; destructive statements (DROP/TRUNCATE/DELETE/UPDATE without WHERE) require typing yes. |
| 4 · Locks | Blocking tree: which session waits on which. |
| 5 · Sessions | pg_stat_activity with state/wait/duration/query. c cancels the query, k terminates the connection, / fuzzy quick-find (by PID, user, database, state or query text). |
| 6 · Roles | Roles with their attributes and connection limit (CONN, ∞ = unlimited). SUPER and BYPASSRLS are marked ⚠ yes — both ignore row-level security. enter manage the selected role (reset password — random 32-char, shown once; set the connection limit; edit attributes: LOGIN, CREATEDB, CREATEROLE, SUPERUSER, REPLICATION, BYPASSRLS; or show access — a per-database report of what the role owns and is granted, schema/table privileges included). n create, g grant / R revoke (pick the database in a fuzzy finder, then the privilege, then confirm the exact SQL), D drop, F force-drop (reassign ownership to a successor, then drop — no data loss), / fuzzy quick-find by name. |
| 7 · Tuning | Config sections (switch with a / s / h): a read-only configuration advisor (shared_buffers, effective_cache_size, work_mem, maintenance_work_mem, max_connections — current vs recommended with a verdict; concrete targets need PGTOWER_HOST_RAM_MB / PGTOWER_HOST_CPUS); an ALTER SYSTEM editor (enter edit / x reset any GUC — validated against type & bounds, applied with pg_reload_conf, / filters, restart-required settings are flagged); and a pg_hba editor (pg_hba_file_rules with parse-error flags; n/e/d add/edit/delete a rule — superuser only, each write is backed up, validated, reloaded and auto-rolled-back if admin login breaks). r refresh. |
curl -fsSL https://pgtower.dev/install | shThe same script straight from GitHub, if you prefer:
curl -fsSL https://raw.githubusercontent.com/9level/pgtower/master/install.sh | sh
It detects your OS/arch, downloads the latest static binary (verifying its
SHA-256) and installs it to /usr/local/bin — no compiler, no runtime
dependencies. The binary runs on any Linux distro (Debian, Ubuntu, Alpine, …)
and macOS; only the CPU architecture matters:
| amd64 (x86_64) | arm64 (aarch64) | |
|---|---|---|
| Linux | ✅ | ✅ |
| macOS | ✅ | ✅ |
| Windows | build from source / WSL | — |
Tweak the install: PGTOWER_INSTALL_DIR="$HOME/.local/bin" or PGTOWER_VERSION=vX.Y.Z.
Prefer to read before you pipe to a shell? It's just install.sh.
Prebuilt binaries are also attached to each
GitHub Release.
git clone https://github.com/9level/pgtower.git
cd pgtower
make build # -> ./pgtower
make install # -> /usr/local/bin/pgtower (sudo)pgtower # first run: the Servers screen opens — press a to add a serverOr, one-off without saving anything:
DATABASE_URL='postgres://user:pass@host:5432/postgres?sslmode=disable' pgtowerPress S any time to switch servers, ? for shortcuts, q to quit.
pgtower checks GitHub for a newer release on startup and, if there is one, asks what to do:
- Update now — downloads the right binary for your OS/arch, verifies its
SHA-256 and replaces the running binary in place. If the install directory
needs root (e.g.
/usr/local/bin), it shows the exact command to finish. Restart pgtower afterwards. - Not now — dismiss for this run.
- Never suggest again — stop asking for good (a marker in your config dir);
re-enable with
update_check: trueinconfig.yml.
Disable the check entirely with update_check: false (config.yml) or
PGTOWER_UPDATE_CHECK=0. Source/dev builds are never nagged.
Don't want the prompt at all? Just re-run the installer — it always grabs the
latest release, verifies the checksum and replaces your binary (your
config.yml is left untouched):
curl -fsSL https://pgtower.dev/install | shPin a version with PGTOWER_VERSION=vX.Y.Z. Installed from source instead?
cd pgtower && git pull && make install.
pgtower keeps its servers and settings in config.yml, which it manages
itself: the Servers screen (S) adds, edits and removes servers and saves
them there (mode 0600, since it may hold passwords). The installer creates
/opt/pgtower/config.yml; pgtower also looks next to the binary, in
~/.config/pgtower/ and in the working directory (full reference:
config.yml.example).
# /opt/pgtower/config.yml
version: 2
default: prod # opened at startup (pgtower -s NAME picks another)
connections:
- name: prod
url: postgres://admin:secret@10.0.0.5:5432/postgres?sslmode=require
tag: prod # dev | staging | prod
- name: local
host: /var/run/postgresql # host, IP, or a unix-socket directory
user: postgres
tag: dev
# refresh_seconds: 5
# scram_iterations: 15000 # PBKDF2 rounds for password-reset hashing
# host_ram_mb: 8192 # host RAM for the Tuning advisor (also per server)
# host_cpus: 4 # host cores for the Tuning advisor (also per server)
# update_check: true # startup "newer release available" promptpgtower --list # show the configured servers
pgtower -s local # open a specific one- Environment variables still work and win:
DATABASE_URL(or the standardPGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE/PGSSLMODE) adds a session-only server namedenvthat opens first and is never written toconfig.yml.PGTOWER_*variables override the settings. - Passwords can stay out of the file: leave the field empty and use
~/.pgpass, or setpassword_env: SOME_VARon the server. - The admin db (the database in the URL,
postgresby default) is where cluster-level queries run (pg_stat_activity,pg_database, replication, locks). pgtower opens additional connections on demand when you browse another database or run a query against a different target (switch it with/). - Search locations:
PGTOWER_CONFIG(an explicit file) orPGTOWER_CONFIG_DIR(a directory) replace the search; otherwise./, the binary's directory,~/.config/pgtower/,/opt/pgtower/,/etc/pgtower/(first hit wins).
pgtower was called pgtui up to v0.9 (renamed in v0.10 because another project already used that name). Same code, same keys, same config format — only the name changed, and the upgrade handles it for you:
| Before (pgtui) | Now (pgtower) | How it moves |
|---|---|---|
pgtui command |
pgtower |
The in-app update or the installer renames the binary; pgtui stays as a symlink so scripts keep working (delete it whenever you like). |
/opt/pgtui/, ~/.config/pgtui/, /etc/pgtui/ |
/opt/pgtower/, ~/.config/pgtower/, /etc/pgtower/ |
Moved on first run; nothing is left behind. If a directory can't be moved (permissions), it is still read in place and pgtower tells you. |
PGTUI_* variables |
PGTOWER_* |
The old names are still read as a fallback; pgtower lists the ones you should rename. |
application_name = 'pgtui' |
'pgtower' |
Update any monitoring filter that relied on it. |
github.com/9level/pgtui |
github.com/9level/pgtower |
GitHub redirects the old URLs. |
A one-time Welcome to pgtower notice summarises what was done on your machine.
Older versions supported a single connection (database_url / host / … at
the top of config.yml, and up to v0.7 a .env file). On the first run of a
newer pgtower this is converted automatically: the connection becomes a named
server (named after its host, set as default), the original files are kept as
config.yml.v1.bak / .env.v1.bak (mode 0600), and a one-time notice says
what was done. Nothing else is needed; delete the .v1.bak files once you no
longer plan to downgrade.
- Read access to
pg_stat_*/pg_databaseis enough for the read-only tabs. To see other sessions' query text on the dashboard/sessions view, connect as a superuser or a member ofpg_monitor. - Management actions need the usual Postgres privileges:
CREATEROLEto create/drop roles,CREATEDBto create databases, and ownership/WITH ADMINto grant. No superuser required. - The Tuning tab is the exception. The settings advisor works for any role,
but
pg_hba_file_rules(the pg_hba viewer) is superuser-only,ALTER SYSTEMneeds a superuser, and editingpg_hba.confneeds a superuser (it writes the file viaCOPY … TO PROGRAM). Without those, the affected sections show as read-only.
Press ? in the app for the full, scrollable list.
| Key | Action |
|---|---|
1–7 |
switch tab |
tab / shift+tab |
next / previous tab |
S / ctrl+o |
servers: switch, add (a), edit (e), delete (d), test (t), default (*) |
? |
help (all shortcuts) |
q / ctrl+c |
quit |
| Databases | |
enter |
database → tables → read-only data |
/ |
fuzzy quick-find in the database / table list |
d |
describe table (columns, types, indexes, constraints) |
n / D |
create / drop database (drop asks for the name) |
| Table data | |
←/→ h/l |
move between columns (horizontal scroll) |
/ |
search the active column (ILIKE '%term%') |
e |
edit the top query bar (read-only) |
| Query | |
i / enter |
focus the SQL editor |
/ or ctrl+t |
switch the target database (filterable list) |
x |
EXPLAIN (plan, without executing) |
ctrl+r / f5 |
run |
| Sessions | |
/ |
fuzzy quick-find (PID, user, database, state, query) |
c |
cancel the session's query (pg_cancel_backend) |
k |
terminate the connection (pg_terminate_backend) |
| Roles | |
/ |
fuzzy quick-find a role by name |
enter |
manage role: reset password / connection limit / edit attributes / show access |
n |
create role/user |
g |
grant to a database — fuzzy-pick the database, choose the privilege (CONNECT / ALL / read-only / read-write / public schema / owner), then confirm the SQL |
R |
revoke from a database (same picker; undoes the default privileges too) |
D |
drop role (asks for the name) |
F |
force-drop: reassign ownership to a successor, then drop — no data loss |
| Tuning | |
a / s / h |
switch section: advisor / settings / pg_hba |
enter / x |
settings: edit (ALTER SYSTEM) / reset a GUC (/ filters) |
n / e / d |
pg_hba: add / edit / delete a rule (superuser) |
r |
re-read pg_settings / pg_hba |
pgtower is built so you can't lose data by accident:
- The query runner classifies every statement: read-only runs immediately,
writes confirm with
y, and critical statements (DROP DATABASE/DROP TABLE/TRUNCATE, orDELETE/UPDATEwithout aWHERE) require typingyes. - Dropping a role or a database opens a confirmation that only proceeds when you type the object's exact name.
- Force-drop never deletes data: it
REASSIGN OWNED/ALTER DATABASE OWNERto a successor role and revokes privileges beforeDROP ROLE. - Passwords are hashed client-side. Both create role and reset
password compute the SCRAM-SHA-256 verifier locally and send only that
in
CREATE/ALTER ROLE, so the plaintext never reaches the server or its logs. Reset generates a random 32-char password (letters and digits only, safe in any terminal) and shows it once, on screen. A non-ASCII typed password is sent as-is so the server can SASLprep it correctly. The PBKDF2 round count defaults to 15000 (stronger than Postgres' 4096) and is configurable viaPGTOWER_SCRAM_ITERATIONS. - Editing
pg_hba.conf(Tuning tab) never leaves you locked out: pgtower backs up the file, writes the change, checkspg_hba_file_rulesfor parse errors, reloads, then opens a fresh admin connection to confirm login still works — any failure restores the backup and reloads. It needs a superuser connection and writes viaCOPY … TO PROGRAM(no adminpack needed). - Admin statements run over the pgx simple protocol (required for
CREATE/DROP DATABASE), with quoted identifiers. - There is no code path that drops a database on its own.
make test # unit + integration (integration self-skips without DATABASE_URL)
make vet
gofmt -l . # should be emptyNeed a throwaway cluster for the integration tests? A docker-compose.yml
(PostgreSQL 18, superuser) ships with the repo:
docker compose up -d
export DATABASE_URL='postgres://postgres:pgtower_test@127.0.0.1:5432/postgres?sslmode=disable'
make test # now runs the live tests too
PGTOWER_HBA_LIVE_TEST=1 go test ./... # also the guarded pg_hba write testSee CONTRIBUTING.md for the full workflow. To debug the UI,
log to a file: PGTOWER_DEBUG=/tmp/pgtower.log ./pgtower.
make build-all VERSION=vX.Y.Z # cross-compile to dist/ (linux/darwin, amd64/arm64)
make release VERSION=vX.Y.Z # validate semver + clean tree, tag, pushPushing a v*.*.* tag runs the CI (.github/workflows/ci.yml): tests, then
build-all, then the binaries are attached to a GitHub Release.
The source code is licensed under the MIT License © 2026 9Level.
The MIT license covers the code only — it does not grant rights to the project's brand. "pgtower", "9Level", and the 9Level logo are trademarks of 9Level; see TRADEMARKS.md. If you fork it, please use a different name.
