Visual TUI for PostgreSQL backup & restore. Dump a production database over an SSH tunnel and restore it into your local Postgres β all from a clean terminal UI, without memorizing
pg_dump/pg_restoreflags. Inspired by lazygit and built as a sibling to lazyports.
π pgflow local β Β· prod β Β· tΓΊnel abierto 25 backup(s) Β· 16:45:07
ββ Backups (25) ββββββββββββββββββββββββββββββββ Detalle βββββββββββββββββββ
β βΎ tienda-web (21) ββ shop_20260610_1645.dump β
β β shop_20260610_1645.dump 1.7MB ββ β
β β shop_20260610_1135.dump 1.7MB ββ carpeta tienda-web β
β β shop_20260610_0801.dump 0B ββ tamaΓ±o 1.7 MB β
β βΈ blog (3) ββ objetos 1004 β
β βΎ api (1) ββ creado 2026-06-10 16:46 β
β β api_20260601_0930.dump 4.2MB ββ estado β vΓ‘lido β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
b backup r restore v verify d delete t tΓΊnel c config ? ayuda q quit
Plataformas soportadas: macOS Β· Linux Β· Windows 10 1809+ β un solo binario, mismo cΓ³digo fuente.
- Un solo binario, cero runtime β Go puro con Bubble Tea. No hay que instalar Python ni Node.
- Flujos guiados β backup (3 pasos) y restore (4 pasos) como asistentes con barra de progreso, resumen y confirmaciΓ³n. Puedes retroceder con
β. - TΓΊnel SSH automΓ‘tico β abre el tΓΊnel a producciΓ³n solo cuando hace falta (vΓa un alias de
~/.ssh/configconLocalForward). - Integridad a la vista β cada dump se verifica con
pg_restore --list; los corruptos salen marcados conβ. - Restore seguro β
--single-transaction(todo o nada). El modo reemplazar (DROP + CREATE) avisa en rojo porque es irreversible. - Errores en cristiano β tΓΊnel caΓdo, credenciales, disco lleno, encoding incompatible, objetos duplicadosβ¦ traducidos a mensajes accionables.
- Modo CLI β
pgflow --list [--json]para scripting.
pgflow se compila desde el mismo cΓ³digo fuente para los tres sistemas operativos. La matriz de soporte:
| SO | VersiΓ³n mΓnima | ssh |
psql / pg_dump / pg_restore |
Instalador | Arquitecturas |
|---|---|---|---|---|---|
| macOS | 10.15 (Catalina) | brew install openssh (o built-in) |
brew install postgresql |
bash install.sh |
amd64 Β· arm64 (Apple Silicon) |
| Linux | glibc 2.31+ (Ubuntu 20.04 / Debian 11 / RHEL 8.4+) | openssh-client |
postgresql-client |
bash install.sh |
amd64 Β· arm64 |
| Windows | 10 build 1809+ Β· 11 Β· Server 2019+ | built-in (OpenSSH client) | postgresql.org/download/windows | .\install.ps1 |
amd64 Β· arm64 |
- TΓΊnel SSH cross-platform.
internal/tunnelcorressh -N <alias>como subprocess de Go (sinssh -fnipkill). Cierra por PID viaProcess.Kill(). Funciona idΓ©ntico en macOS, Linux y Windows β elssh.exeque viene con Windows 10 1809+ respeta los mismos flags. - Build tags OS-especΓficos, sin condicionales dispersos. Solo hay un archivo con cΓ³digo distinto por OS:
tunnel_windows.go(8 lΓneas). El resto del cΓ³digo es portable sin condiciΓ³n.tunnel_unix.goβ//go:build !windowsβ compila en darwin + linuxtunnel_windows.goβ//go:build windowsβ compila solo en windows
- Rutas. Todo via
filepath.Join+os.UserHomeDir()β Go resuelve%USERPROFILE%en Windows,$HOMEen Unix, asΓ que~/.pgflow.confy~/.ssh/config"simplemente funcionan". - TUI. Bubble Tea v1.3+ trae soporte Windows built-in (
tty_windows.goen la lib, abreCONIN$y activa VT input/output). - No hay flash de consola en Windows. Al spawnear
ssh.execomo hijo, le aplicamosCREATE_NO_WINDOW(0x08000000) para que no aparezca una ventana parpadeante. - Cero dependencias de shell. No usamos
pkill,bash, ni nada que no exista igual en los 3 OSes. Todo va poros/exec.
.github/workflows/ci.yml corre en cada push/PR:
- Job
test: matrixubuntu-latestΓmacos-latestΓwindows-latestβgo build+go vet+gofmt -l+go test -race. - Job
cross: desdeubuntu-latest, cross-compila los 6 targets (darwin/linux/windowsΓamd64/arm64) conCGO_ENABLED=0.
Si quieres probarlo en tu mΓ‘quina antes de instalar:
git clone https://github.com/ander0code/pgflow.git ~/pgflow
cd ~/pgflow
go run . # necesita un TTY real (no pipelines)make build-all # β dist/pgflow-{darwin,linux,windows}-{amd64,arm64}[.exe] + checksums.txtLos binarios Windows llevan sufijo .exe; los Unix no.
Cada release publica dist/checksums.txt con el SHA256 de cada binario.
Los instaladores (install.sh / install.ps1) lo descargan y verifican
automΓ‘ticamente; si la firma no coincide, no instalan. Para verificar
manualmente tras descargar:
sha256sum -c dist/checksums.txt # macOS / LinuxGet-FileHash dist\pgflow-windows-amd64.exe -Algorithm SHA256 # Windows- Go 1.25+ (solo para compilar).
- En el
PATH:psqlΒ·pg_dumpΒ·pg_restore(cliente PostgreSQL).ssh(cliente OpenSSH).
| SO | Comando |
|---|---|
| macOS | brew install go postgresql |
| Debian / Ubuntu | sudo apt install golang postgresql-client openssh-client |
| Fedora / RHEL | sudo dnf install golang postgresql openssh-clients |
| Arch / Manjaro | sudo pacman -S go postgresql-clients openssh |
| Windows 10/11 | Instalar PostgreSQL. OpenSSH ya viene integrado (si no: Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0). |
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/ander0code/pgflow/main/install.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/ander0code/pgflow/main/install.ps1 | iexDescarga el binario de la ΓΊltima release
a ~/.local/bin/pgflow (Windows: %LOCALAPPDATA%\pgflow\pgflow.exe). Si ese directorio no estΓ‘ en tu
PATH, aΓ±Γ‘delo (ver abajo) y luego ejecuta pgflow.
# macOS / Linux β aΓ±ade a ~/.zshrc o ~/.bashrc:
export PATH="$HOME/.local/bin:$PATH"# Windows β PATH de usuario:
[Environment]::SetEnvironmentVariable('Path',"$env:LOCALAPPDATA\pgflow;" + [Environment]::GetEnvironmentVariable('Path','User'),'User')git clone https://github.com/ander0code/pgflow.git ~/pgflow
cd ~/pgflow
make install # build + copia a ~/.local/bin (Windows: .\install.ps1)Los instaladores install.sh / install.ps1 tambiΓ©n funcionan desde el clon: intentan descargar el
release y, si falla, compilan del cΓ³digo (requiere Go).
Sin instalar: go run . Β· desinstalar: bash uninstall.sh (Windows: .\uninstall.ps1).
La configuraciΓ³n vive en ~/.pgflow.conf (mismo formato que la versiΓ³n
anterior, asΓ que tu config existente sigue funcionando). EdΓtala a mano β
copia pgflow.conf.example β o desde la app en
Configurar conexiones (c).
El alias de producciΓ³n debe traer su propio LocalForward en ~/.ssh/config:
Host mi-servidor-db
HostName 1.2.3.4
User deploy
LocalForward 5433 localhost:5432
Las contraseΓ±as pueden ir en
~/.pgflow.conf(conchmod 600en Unix) o, mejor, en~/.pgpass.~/.pgflow.confestΓ‘ en.gitignore; nunca se sube al repo.
pg_dump se ejecuta con el rol que configures y necesita poder leer todas
las tablas de la base (hace LOCK TABLE β¦ IN ACCESS SHARE MODE sobre todas a la
vez). Un rol de aplicaciΓ³n normalmente solo accede a parte de la base, asΓ que el
backup falla con permission denied for table β¦.
Dale lectura completa al rol, en el servidor y como admin:
-- PostgreSQL 14+
GRANT pg_read_all_data TO tu_rol;En versiones anteriores: GRANT SELECT ON ALL TABLES IN SCHEMA public TO tu_rol;
(mΓ‘s ALTER DEFAULT PRIVILEGES β¦ GRANT SELECT β¦ para tablas futuras), o usa un
rol superusuario para el backup.
pgflow # abre la TUI
pgflow --list # lista los backups (texto)
pgflow --list --json # lista los backups (JSON, para jq/pipe)
pgflow --version
pgflow --help| Tecla | AcciΓ³n |
|---|---|
β β / j k |
navegar |
g / G |
primero / ΓΊltimo |
enter / space |
colapsar carpeta Β· restaurar el dump seleccionado |
b |
backup de producciΓ³n (asistente) |
r |
restaurar a local (asistente) |
v |
verificar la integridad del dump |
d |
borrar un dump (con confirmaciΓ³n) |
t |
abrir / cerrar el tΓΊnel SSH |
c |
configurar / probar conexiones |
^r |
refrescar |
β / h |
(en los asistentes) volver un paso |
esc |
cancelar el asistente / cerrar modal |
q / ctrl+c |
salir |
βββββββββββββββββββββββββββββββββββ
β main.go β
β flags CLI + arranque Tea.New β
ββββββββββββββββββ¬βββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β internal/tui β
β Model Β· Update Β· View β
β pantallas Β· modales Β· commands β
βββ¬βββββββββββ¬βββββββββββ¬βββββββββ¬β
β β β β
βββββββββΌββββ ββββββΌβββββ βββββΌβββββ ββΌββββββββββββββ
β config β β pg β β tunnel β β backups β
β ~/.pgflow β β psql β β SSH β β escaneo β
β .conf β β pg_dump β βLocal- β β del dir β
β β βpg_restoreβ βForward β β .dump β
βββββββ¬ββββββ ββββββ¬βββββ βββββ¬βββββ ββββββββββββββββ
β β β
ββββββββββββββ΄βββββββββββ
config (credenciales)
Reglas de dependencia (sin ciclos):
tuiβ {config,pg,tunnel,backups}backupsβpgpgβconfigtunnelβconfig
| Archivo | OS | Responsabilidad |
|---|---|---|
main.go |
todos | Flags CLI (--list / --json / --version / --help) + tea.NewProgram(..., WithAltScreen()) |
internal/config/config.go |
todos | Lee / escribe ~/.pgflow.conf (formato shell). Tipos Config y Conn. Load() / Save(). expandHome() resuelve $HOME, ${HOME}, ~. |
internal/pg/pg.go |
todos | Wrappers de psql / pg_dump / pg_restore. Dump/restore en streaming con --verbose lΓnea por lΓnea. Verify con pg_restore --list. CountTables. Clasificadores de error en espaΓ±ol (classifyConnError, classifyDumpError, classifyRestoreError, deniedObject) β el caso de permisos parsea el objeto denegado y sugiere GRANT pg_read_all_data. |
internal/tunnel/tunnel.go |
todos | API pΓΊblica: IsUp, Ensure, Close, IsOpen. Maneja el ssh subprocess con sync.Mutex. Reap automΓ‘tico en caso de fallo. |
internal/tunnel/tunnel_unix.go |
!windows |
//go:build !windows β hideWindow es no-op en Unix. |
internal/tunnel/tunnel_windows.go |
windows |
//go:build windows β hideWindow aplica CREATE_NO_WINDOW para que ssh.exe no haga flash de consola. |
internal/tunnel/tunnel_test.go |
todos | Lifecycle tests con el patrΓ³n TestHelperProcess (re-ejecuta el binario como ssh fake). Cubre: puerto cerrado, ya arriba, ciclo completo ensure/close, reap en fallo. |
internal/backups/backups.go |
todos | Escanea BackupDir β []Folder con []Dump (size, mtime, validez). Scan, Total. Verifica cada dump con pg.Verify. |
internal/tui/model.go |
todos | Model, Init, Update, View. Key handling. Wizards: backup 3 pasos, restore 4 pasos (con sub-paso 31). Estado de salud (local/tΓΊnel/prod). Splash, modales, status bar. |
internal/tui/render.go |
todos | Toda la UI: dashboard, detail panel, wizards, modales, splash, run-screen con logs en vivo. Helpers: humanSize, truncate, truncateLeft, padR, padLine. |
internal/tui/styles.go |
todos | Paleta Catppuccin Mocha + morado Charm #7D56F4. Estilos lipgloss (modales sin fondo, paneles con borde, fila seleccionada morada). |
internal/tui/commands.go |
todos | Funciones que devuelven tea.Cmd (trabajo async). PatrΓ³n chan logEvent para dump/restore en vivo con waitForLog. Timers (tickCmd, spinnerTickCmd, splashTickCmd, statusClearCmd). |
internal/tui/messages.go |
todos | Tipos tea.Msg: scanDoneMsg, statusDoneMsg, prodDBsMsg, localDBsMsg, streamStartedMsg, logEventMsg, verifyDoneMsg, deleteDoneMsg, tunnelDoneMsg, connTestMsg. |
internal/tui/render_smoke_test.go |
todos | TestViewAcrossStates (recorre todas las pantallas y verifica que View() no entra en pΓ‘nico) + TestSnapshot (imprime el render con ANSI quitado, ΓΊtil con -v). |
Makefile |
n/a | Targets: build, run, test, vet, tidy, install, uninstall, clean, build-all, release. PLATFORMS = los 6 targets. |
install.sh / uninstall.sh |
unix | Instalador bash para macOS / Linux (Homebrew, apt). |
install.ps1 / uninstall.ps1 |
windows | Instalador PowerShell para Windows. Verifica Go y herramientas runtime, build, copia a %LOCALAPPDATA%\pgflow, hint para aΓ±adir al PATH. |
.github/workflows/ci.yml |
n/a | CI matrix ubuntu Γ macos Γ windows + cross-build de los 6 targets. |
pgflow.sh |
n/a | VersiΓ³n original en Bash (legacy, referencia). |
pgflow.conf.example |
n/a | Plantilla de config (se commitea; sin credenciales). |
AGENTS.md |
n/a | GuΓa para que un agente (humano o IA) entienda y modifique el repo rΓ‘pido. |
streamDumpCmd (en internal/tui/commands.go) crea un chan logEvent con buffer,
lanza una goroutine que corre pg.DumpStream(..., onLine) β cada lΓnea de stderr de
pg_dump --verbose se manda al canal β y termina con logEvent{done:true, ...}.
Devuelve streamStartedMsg{ch}. El modelo guarda el canal y emite
waitForLog(ch), que bloquea en <-ch y devuelve logEventMsg. En cada evento
no-final el modelo vuelve a emitir waitForLog; en el final corta. La pantalla de
ejecuciΓ³n (renderRunScreen) muestra la cola de lΓneas + spinner + tiempo.
internal/pg/pg.go traduce stderr de psql/pg_dump/pg_restore a mensajes
accionables en espaΓ±ol. Ejemplos:
| Stderr crudo | Mensaje pgflow |
|---|---|
connection refused |
conexiΓ³n rechazada β ΒΏel tΓΊnel estΓ‘ caΓdo? (pulsa t) |
password authentication failed |
contraseΓ±a incorrecta |
permission denied for table auth_group |
el rol Β«produserΒ» no puede leer la tabla Β«auth_groupΒ». GRANT pg_read_all_data TO produser; β PostgreSQL 14+ |
no space left on device |
sin espacio en disco |
database already exists (en restore) |
objetos duplicados β usa 'crear base nueva' |
El caso de permisos parsea el tipo de objeto (table, schema, sequence,
view) y el nombre, y construye una sugerencia de GRANT.
- Nuevo campo de config: agrΓ©galo a
Config/Save/Load(config.go) y abuildCfgFields(model.go, con susection). - Nueva acciΓ³n de dashboard: aΓ±ade un
caseenhandleDashboardKey(model.go) y, si es async, un comando encommands.go+ un mensaje enmessages.go+ sucaseenUpdate. - Nuevo paso de asistente: ajusta la mΓ‘quina
m.stepen*Select/*Backycrumb(). - Nuevo modal: aΓ±ade la constante al enum
modal, manΓ©jalo enhandleModalKeyyrenderModal.
Tras cualquier cambio de UI, corre el snapshot test para verlo:
go test ./internal/tui/ -run TestSnapshot -vEl nombre del archivo .dump se arma con una plantilla por base de datos (tokenizable), un prefijo por carpeta (opcional), y un contador autoincremental (opcional, si la plantilla lo usa). Todo vive en ~/.pgflow.json.
Defaults:
- Con prefijo de carpeta:
{prefix}-{db}-{datetime}βTIENDA-WEB-shopdb-20260621_183045.dump - Sin prefijo:
{db}_{datetime}βshopdb_20260621_183045.dump
Tokens disponibles en la plantilla:
| Token | Valor |
|---|---|
{db} |
nombre de la base |
{date} |
fecha en formato YYYYMMDD |
{time} |
hora en formato HHMMSS |
{datetime} |
fecha + hora, YYYYMMDD_HHMMSS |
{seq} |
contador autoincremental de 3 dΓgitos (001, 002, ...) |
{prefix} |
el prefijo configurado en la carpeta destino |
CΓ³mo editarlo: en el paso Confirmar del backup:
eβ editar el nombre del archivo concreto (no persistente)tβ editar la plantilla por db (se guarda en~/.pgflow.json)pβ editar el prefijo de la carpeta destino (se guarda)
Si la plantilla usa {seq}, el contador se autoincrementa automΓ‘ticamente cada vez que un backup termina OK.
- Scroll parcial. El dashboard y los pickers tienen scroll implementado (
windowBounds+scrollHint) β el cursor se mantiene visible y aparece un indicadorpos/total ββ. La pantalla de log en vivo durante dump/restore (renderRunScreen) muestra los ΓΊltimos N lΓneas sin scroll interactivo (suficiente para esa vista). - No se adoptan tΓΊneles externos. Si abres el tΓΊnel por tu cuenta (sin que pgflow lo gestione), pgflow lo detecta con
IsUppero al pulsarcpara cerrarlo reporta "puerto abierto por otro proceso β pgflow no lo controla" en vez de intentar matarlo. - Windows < 10 1809 no soportado porque no trae
ssh.exebuilt-in. Funciona si instalas OpenSSH manualmente, pero no es ruta soportada oficialmente.
MIT.
La versiΓ³n original en Bash sigue disponible en
pgflow.sh.