pgtower is a keyboard-first PostgreSQL administration TUI (Go 1.26, Bubble
Tea). Single static binary, no CGo. Public repo: github.com/9level/pgtower.
make build # -> ./pgtower (dev build, version "dev")
make install # -> /usr/local/bin/pgtower (dev build)
make vet # go vet ./... (must be clean)
gofmt -l . # must print nothing (gofmt -w . to fix)
make test # unit + integration; live tests self-skip without DATABASE_URLLive/integration tests need a throwaway cluster:
docker compose up -d
export DATABASE_URL='postgres://postgres:pgtower_test@127.0.0.1:5432/postgres?sslmode=disable'
make testBefore any commit: gofmt -l . empty, go vet ./... clean, go test ./... green.
Resolution order, highest first: environment variables → config.yml →
defaults. There is no .env-file support — env vars only, plus the file.
config.yml(formatversion: 2) holds a list of namedconnectionsplus global settings, and is written by the app (Servers screen,S) — keepconfig.Store.Savethe only writer; it is atomic and0600. Template and key reference:config.yml.example.- Searched in
./, the binary's dir,~/.config/pgtower/,/opt/pgtower/,/etc/pgtower/;PGTOWER_CONFIG(file) orPGTOWER_CONFIG_DIR(dir) replace the search (tests rely on this for isolation). DATABASE_URL/PG*add a session-only connection namedenv; it is never saved.PGTOWER_*override the settings.- pgtower was pgtui up to v0.9.
internal/config/legacy.gostill readsPGTUI_*(afterPGTOWER_*, viaconfig.Env) and moves the pgtui config dirs on load;internal/update/rename.gorenames a binary started aspgtuiand leaves apgtuisymlink. Always read settings throughconfig.Env. - Legacy (v0.8-) single-connection files and pre-v0.8
.envfiles are migrated on load byinternal/config/migrate.go(backup*.v1.bak, then rewrite). Any future format change must follow the same pattern: bumpFileVersion, migrate + back up, and surface a one-time notice (config.Migration).
- Land everything on
master; tree clean;gofmt/vet/testgreen. make release VERSION=vX.Y.Z— validates semver + clean tree, tags, pushes the tag.git push origin master—make releasepushes only the tag; sync the branch.- CI (
.github/workflows/ci.yml, onv*.*.*tags) runsmake build-alland attachesdist/*to a GitHub Release with auto-generated notes. - Verify:
gh release view vX.Y.Zlists 8 binaries +SHA256SUMS(4 whileLEGACY_BINARYis empty).
The version exists only in the git tag (injected via -ldflags main.version); nothing in the source needs editing to bump it. Release assets
must stay named pgtower-<version>-{linux,darwin}-{amd64,arm64} and
SHA256SUMS — both install.sh and the in-app self-updater
(internal/update) fetch them by name. Until LEGACY_BINARY is dropped from
the Makefile (planned for v0.12), every release also carries identical
pgtui-<version>-* copies so v0.9 self-updaters can reach pgtower: a release
then lists 8 binaries + SHA256SUMS. Doc command-examples use a vX.Y.Z
placeholder so they never go stale.
- Do NOT add a
Co-Authored-By: Claudetrailer (nor "Generated with Claude Code" in PR bodies). This repo is public and the owner wants no such attribution. Messages: imperative, present tense, Conventional-Commits style (feat:,fix:,docs:…). - Keep it dependency-light and destructive-action-guarded: dropping a role/database always requires typing the exact name; never add a bypass.
- Match the surrounding style (naming, comment density, idioms).
main.go entrypoint, flags, version
internal/config config.yml (connections + settings), env, legacy migration
internal/db pgx pools + all SQL (queries, admin, describe, safety, scram, hba, settings),
connection-error diagnosis (connerr.go)
internal/ui Bubble Tea model, sessions, Servers screen, tabs, reusable modals
(confirm/form/alert/menu/finder)
internal/update GitHub release check + in-place self-update
Each tab implements tabView (internal/ui/model.go). Tabs belong to a
session (internal/ui/session.go) — one per connected server, rebuilt on
every switch. Every command a tab returns must go through session.scope
so results arriving after a switch are dropped instead of rendering under the
wrong server. Model-level modals (help, Servers, notices, update prompt, quit
confirmation) live on Model; tab-level ones are fields on each tab. q asks
to confirm before quitting; ctrl+c hard-quits.