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.