Logical Replication

pg_vault_tde ships a logical decoding output plugin, pg_vault_tde_pgoutput, so that encrypted_heap tables can be published to logical replication subscribers in plaintext, even though their on-disk storage and WAL are ciphertext. Subscribers see normal decrypted rows — no client-side decryption logic is needed.

Why a Plugin Is Needed

The table access method’s decrypt-on-read callbacks run inside the query executor. A logical decoder reads raw WAL records directly, bypassing the executor entirely — so without a dedicated plugin, a subscriber would receive encrypted garbage instead of usable rows.

Setting It Up

The plugin wraps the built-in pgoutput transparently: it decrypts each tuple in place before delegating to pgoutput for serialization, so the wire protocol emitted is identical to standard pgoutput — it works with both pg_recvlogical and a native CREATE SUBSCRIPTION/CREATE PUBLICATION workflow, with no special syntax on the publisher side beyond normal publication setup:

CREATE PUBLICATION mypub FOR TABLE secrets;

TOAST Columns Need an Extra Step

Externally-TOASTed column values need more than in-place decryption: the core reorder buffer reassembles a TOASTed value from its chunks before any output-plugin callback runs, and it cannot do that correctly against still-encrypted chunks. pg_vault_tde resolves this with a custom WAL resource manager, gated by a dedicated GUC:

pg_vault_tde.toast_custom_rmgr = on   # PGC_POSTMASTER — requires a restart

This requires pg_vault_tde to already be in shared_preload_libraries (true by default for the extension) and defaults to off. Enable it if any published table has TOASTable columns whose out-of-line values need to reach subscribers.

Requirements and Supported Operations

Operation Requirement
INSERT (inline values, TOASTed values, bursts) pg_vault_tde.toast_custom_rmgr = on if the table has TOASTable columns
Initial table sync (COPY) Works automatically via the normal TAM decrypt-on-read path
UPDATE / DELETE REPLICA IDENTITY FULL + a primary key, mandatory

Why REPLICA IDENTITY FULL + a Primary Key Is Mandatory

With REPLICA IDENTITY DEFAULT, PostgreSQL core derives the replica identity by reading the encrypted old tuple as if it were the identity key — producing a constant, meaningless value. The subscriber would then silently target the wrong row on UPDATE/DELETE. Tables without a primary key are unsupported for the same reason (there is no key to match on). These are documented, structural limitations of the extension API — not bugs — because to PostgreSQL core, an encrypted_heap tuple is just an opaque ciphertext blob wherever it tries to read a single column value directly.

ALTER TABLE secrets REPLICA IDENTITY FULL;   -- required on any table that is
                                              -- a logical-replication UPDATE/DELETE source

Structural Limitations

  • Aborted, non-streamed transactions. A TOAST-writing transaction that reaches a full snapshot and then aborts without ever being streamed leaves its captured chunks in memory until the decoding process exits — there is no output-plugin hook for non-streamed aborts. This is a slow, per-abort memory leak in the decoding process, not a per-row leak; restart the logical decoding worker/walsender periodically if your workload has many such aborts.
  • Resource manager ID. The custom WAL resource manager currently uses the experimental ID RM_EXPERIMENTAL_ID (128); a stable ID will be reserved with the PostgreSQL community before this feature reaches general availability. Confirm no other extension on the same server also claims this experimental ID.

See Also