Changelog
Every notable change to pgpipe, newest first. The current release is v2.1.0 (13 June 2026). Format follows Keep a Changelog; pgpipe follows Semantic Versioning.
pgpipe Changelog
All notable changes to pgpipe are documented here.
The format is based on Keep a Changelog and pgpipe adheres to Semantic Versioning.
This file is the canonical source of truth. It ships inside every .deb / .rpm
at /usr/share/doc/pgpipe/CHANGELOG.md and is rendered at
https://www.pghorizon.com/pgpipe/changelog/.
“Unreleased” lists changes that are merged but have not yet shipped in a numbered release.
Unreleased
2.1.0 - 2026-06-13
A dashboard release: a top-to-bottom overhaul of the built-in web UI — visual system, information architecture, DLQ management, accessibility, and a sweep of honesty fixes so the dashboard never claims more than the engine actually does. No configuration, CLI, or wire changes; the replication engine is unchanged from v2.0.0, so upgrading is a drop-in binary/package replacement.
Added
- DLQ management on the dashboard: the unresolved-entry count is surfaced on the Errors card and as a nav badge, with a Discard-all drain action, per-entry copy buttons, and relative timestamps. “Mark Resolved” is renamed to Discard to match what it actually does.
- A status hero with a dedicated Throughput card and honest sparklines, plus a detail strip showing replication-slot liveness, a copyable LSN, and checkpoint age.
- Hash-based tab routing: the active tab lives in the URL, so refresh, the
browser Back button, and deep links to a specific tab (e.g.
#errors) all work, and screen readers getaria-currenton the active nav item. - A responsive breakpoint that collapses the sidebar to an icon rail at phone width, plus a keyboard-operability baseline across the dashboard.
- Logs tab: a catch-up pill, an honest pause state, a debounced filter, and a download button.
- Setup wizard: an unload guard, recovery from an expired token or a gone server, table search, copy-from-source, and a finish line that shows the saved config path and the exact start command.
- Config editor: tab ergonomics (a default-open section and expand-all), a persistent restart-required banner after a save, busy states on every async action button, and read-only fields that explain why they can’t be edited.
- The real build version is shown in the UI instead of a hardcoded string.
Changed
- A complete visual refresh built on semantic design tokens (color, type,
spacing, mono), with inline styles swept into reusable components, pill radii
standardized, emoji iconography replaced by inline SVGs (plus an SVG favicon),
and native dark browser chrome via
color-scheme/theme-color. - Reworked navigation information architecture with a distinct identity per
view; bespoke buttons folded into one
.btnsystem; a skeleton first paint. - htmx is now vendored into the binary and served from
'self'; the Content-Security-Policy is tightened to drop theunpkgCDN origin. - The setup wizard is de-forked from its own stylesheet onto the shared
theme.cssdesign system.
Fixed
- Polling refreshes no longer destroy in-progress user state, and session expiry preserves mid-edit work and explains itself; stale content degrades visually and Retry is non-destructive.
- The login page no longer consumes login rate-limit budget on page load.
- Honesty: removed the DDL Detection section from the config editor (DDL replication is not implemented in this build), and corrected three false capability claims in the embedded Docs tab.
- Accessibility: ARIA live regions, proper input labels, a global reduced-motion
respect,
--text-mutedraised to WCAG AA contrast, and tabular numerals on live values. - The pre-auth flash of dashboard chrome is gated, and the user row is hidden when there is no session.
- Tables: the add-table form is clarified (labels, what “snapshot” means, Enter to submit), and per-table read counts are labeled “since start” so a restart reading zero is no longer mistaken for a stall.
- Lag and uptime now format with hour/day branches; severity thresholds are explained; a fully-caught-up pipeline is colored green; the initializing phase is styled, with a neutral fallback for unknown states.
- Error toasts persist until dismissed (hover pauses auto-dismiss); first-contact empty states for Tables and the DLQ; invalid duration edits are rejected instead of silently dropped; and the dead-end “Retry Save” button is replaced with “Back to editing”.
2.0.0 - 2026-06-10
Security
- BREAKING:
/metricsis now closed by default. The endpoint honoursmetrics.{enabled,path,public,auth_token}; a default install requires the auto-generated bearer token printed on first run, instead of exposing metrics openly. Existing Prometheus scrapers must send the bearer token, or setmetrics.public: trueto restore the old open behaviour. This default change is the reason this release is a new major version.
Fixed
- Destination upsert-key correctness. A chain of fixes closes the
SQLSTATE 42P10(“no unique or exclusion constraint matching the ON CONFLICT specification”) class that previously surfaced per-row at write time (DLQ-quarantined or pipeline abort) instead of as a clear startup error:- Name the
ON CONFLICTarbiter from the real primary key rather than the replica-identity keys. - Correct
REPLICA IDENTITY FULLupserts and make destructive snapshots crash-safe. - Validate the destination upsert key at startup and mirror
USING INDEXunique constraints, failing loudly with a copy-pasteableALTER TABLE ... ADD CONSTRAINT ... UNIQUEfix. - Exclude
DEFERRABLEconstraints from the arbiter check — PostgreSQL rejects them asON CONFLICTarbiters at write time (SQLSTATE 55000). - Rewrite the DLQ 42P10 quarantine hint to point at the destination
constraint. It previously told operators to check that the source’s
REPLICA IDENTITYis notFULL— a diagnosis for the originalFULL→ 42P10 bug that the arbiter registry has since fixed (FULLis now fully supported), so following the old hint led to fixing a non-problem.
- Name the
- Honour all snapshot modes via a startup planner, and guard a destructive
reset against an active replication slot; skip the heartbeat seed for
snapshot_only. - Reject an empty exported-snapshot name before truncating the destination.
A destructive snapshot opens the slot, truncates the destination, then
copies — but the copy is a no-op when the snapshot handle’s name is empty.
An empty name would therefore truncate, copy nothing, checkpoint the empty
result as success, and start streaming: silent data loss.
BeginSnapshotnow fails before the truncate, leaving the destination intact. (It cannot occur against real PostgreSQL, which always exports a snapshot name, but the invariant was previously unguarded.) - Seed the destination heartbeat row after the snapshot rather than before.
In
initial+strictmode the snapshot’sCOPYof the heartbeat row collided with a pre-seeded row, aborting the first start with a duplicate key.
1.1.4 - 2026-06-02
Fixed
- Wizard-saved configs no longer crash systemd installs with a “read-only
file system” error on first start. The setup wizard previously wrote
state.boltdb.path: pgpipe-state.db(a relative path). Under systemd’sProtectSystem=strict, the service’s working directory/is read-only, so the relative path resolved to/pgpipe-state.dband bbolt could not create the file. The unit then restart-looped untilStartLimitBurstgave up and the dashboard never came up.
Changed
pgpipe.servicenow setsWorkingDirectory=/var/lib/pgpipe(the directory systemd already grants write access to viaStateDirectory=pgpipe), and the setup wizard reads$STATE_DIRECTORYfrom systemd and writes an absolutestate.boltdb.pathinto/etc/pgpipe/pgpipe.yaml. Configs written via the wizard now work whether started by systemd or run by hand.
Upgrade notes
Fresh installs: drop in the v1.1.4 .deb / .rpm. No extra steps.
Already running v1.1.3 or earlier on systemd with a config saved by the
setup wizard — the relative path is baked into your pgpipe.yaml. Patch it
once and restart:
sudo cp /etc/pgpipe/pgpipe.yaml /etc/pgpipe/pgpipe.yaml.bak
sudo sed -i 's|pgpipe-state\.db|/var/lib/pgpipe/pgpipe-state.db|' \
/etc/pgpipe/pgpipe.yaml
sudo systemctl reset-failed pgpipe.service
sudo systemctl start pgpipe.service
sudo journalctl -u pgpipe -f # expect: snapshot complete -> WAL streaming
The workaround is forward-compatible — the absolute path keeps working after you upgrade, so there is nothing to undo.
Docker / manual runs: not affected — no ProtectSystem=strict, the
working directory was already writable.
1.1.3 - 2026-05-22
Fixed
- Setup wizard: the Test Connection result now displays and the Next
button is correctly enabled after a successful test. The handler had set
the result element’s
display: noneas an inline style before fetching, which left the result hidden and the wizard unable to advance.
1.1.2 - 2026-05-21
Fixed
- Setup wizard error reporting. The test-connection, list-schemas, and list-tables handlers now warn-log failures and info-log successes; connection errors are classified by SQLSTATE / transport and the UI shows a plain-English summary, remediation, and collapsible technical details.
list-schemas/list-tablessurface real errors instead of silently showing “No schemas/tables found”.- systemd packaging:
StartLimitBurst/StartLimitIntervalSecmoved to the[Unit]section (fixes the restart-burst loop), and/etc/pgpipeis added toReadWritePathsand ownedpgpipe:pgpipeso the wizard’s Save endpoint can writepgpipe.yaml.
1.1.1 - 2026-05-19
Fixed
- Fix a Go 1.22
net/http.ServeMuxpattern panic that crash-looped the setup wizard on startup.
1.1.0 - 2026-05-17
Added
- Opt-in
session_replication_role: replicafor the destination pool. SkipsBEFORE/AFTER ROWtriggers and FK re-checks on already-validated replicated rows via a pgxpoolAfterConnecthook. Default empty preserves existing behaviour; requiresSUPERUSERon the destination. fsync_modeknob for the BoltDB state store.- Writer phase-level Prometheus timing histograms (
pgpipe_writer_build_seconds,pgpipe_writer_send_seconds,pgpipe_writer_commit_seconds) for diagnostic visibility into build / send / commit latency.
Changed
tx_group_size: 50is now the default and is respected in strict ordering; the writer is op-type aware.- Parallel-mode throughput scaling: hot-path pipeline counters are now lock-free atomics, removing the single-mutex cap that limited parallel apply to roughly 8 workers.
- Faster writer and snapshot paths: zero-copy string views in the writer hot
path, per-row primary-key extraction with a single-table fast path,
count(*)replaced withreltuplesfor snapshot sizing, and dedicated source connections for heartbeat / slot-lag / admin roles. - Corrected the throughput claim: pgpipe sustains roughly 95% of native PostgreSQL logical-replication apply throughput, with strict per-transaction ordering preserved. Earlier “faster than native” figures are withdrawn.
Deprecated
- Runtime DDL replication is documented as not implemented and is no
longer advertised as on-by-default. The
internal/ddlpackage is scaffolding with no production code path: the WAL decoder never emits a DDL event, so sourceALTER TABLEstatements are not replicated. The destination schema is still created automatically on first run.
Fixed
- The source
setup_schemaand the heartbeat table it references are now created before the publication references them, instead of failing on a missing relation.
1.0.1 - 2026-04-30
Maintenance build over v1.0.0. There are no source-tracked changes between the v1.0.0 and v1.0.1 builds — see the v1.0.0 entry for the shipped feature set.
1.0.0 - 2026-04-30
First public release. pgpipe streams row changes from one PostgreSQL database to another in real time, with strict per-transaction ordering, a built-in web dashboard, a dead-letter queue, and Prometheus metrics.
Added
- Web dashboard with a login page and a first-run setup wizard.
- Live table management — add and remove replicated tables from the UI, with table-level filtering.
- Dead-letter queue with a REST API: failed events are captured with full context and can be inspected, retried, and replayed; the UI exposes expand / retry row actions.
- Observability: real-time sparkline charts, snapshot progress bars, a WAL-byte replication-lag metric, a stale-data banner, and an SSE log-tail viewer with a config diff / validation preview.
- Pause / resume for the pipeline.
- Prometheus metrics, plus
/healthand/readyendpoints for liveness and readiness probes. - HTTPS / TLS for the dashboard via
server.tls.cert_fileandserver.tls.key_file. - Startup safety checks: PostgreSQL version detection and replica-identity validation, both reported clearly at startup.
- Bidirectional heartbeat verification and replication-slot drift detection.
.deband.rpmpackages, and cross-compiled binaries for all 8 supported platforms.THIRD_PARTY_LICENSES.txtbundled alongside every binary.
Changed
- 5x–10x write throughput via prepared-statement caching and transaction grouping.
- Single-writer strict ordering by default for consistency, with async commit on the destination.
Fixed
- Total-rows counters now survive restarts, and the events-per-second readout no longer shows “idle” while replicating.
Security
- Dashboard authentication is enabled by default, with a random admin password generated and printed on first run.
- Persistent JWT secret so dashboard sessions survive restarts.
- Rate limiting on
/api/auth/login, with trusted-proxyX-Forwarded-Forhandling. - Security headers on all dashboard responses: CSP, HSTS,
X-Frame-Options,X-Content-Type-Options, andReferrer-Policy.