Queries icon
Batch icon
Query icon
If icon
Fail icon
Delete icon
Log icon
SlackIncomingWebhook icon
Trigger icon

Replicate PostgreSQL inserts, updates and deletes with Debezium CDC

Keep a PostgreSQL replica in sync with Debezium CDC. Merge inserts, updates and deletes by LSN, keep TOAST values and stop on mass deletes.

Categories
Data

Debezium captures PostgreSQL row changes without Kafka Connect, and a JDBC Batch task can append them to a table. A replica that matches the source needs more: updates and deletes in order, and no old event written over a newer row. This flow stages each batch of change events and merges it into the replica with one SQL statement.

How it works

  1. The Debezium PostgreSQL Trigger reads the replication slot every minute. The first run copies a snapshot of public.customers. Each event has the primary key, a deleted flag and the source LSN in metadata.
  2. create_replica_tables creates cdc.customers, the staging table cdc.customers_changes and the view cdc.customers_live if missing.
  3. stage_changes loads the events into the staging table with the execution ID. metadata goes into a jsonb column for the LSN.
  4. count_changes keeps the last event per key. mass_delete_guard stops a batch that deletes more than max_deletes_per_batch rows, unless the KV approval is set.
  5. apply_changes upserts the last event per key only when its LSN is newer than the row's _cdc_lsn. A delete sets _cdc_deleted. An unchanged TOAST value (__debezium_unavailable_value) keeps the replica value.
  6. The flow removes a used approval and logs the counts. On failure, alert_failure sends one Slack message with the task and the error.

What you get

  • cdc.customers_live with the same rows and values as the source.
  • _cdc_lsn, _cdc_deleted and _cdc_applied_at for audits and incremental reads.
  • Batches applied in order (concurrency limit 1, QUEUE). A restarted or late batch never replaces a newer row or brings back a deleted row.
  • A mass-delete stop with a one-time approval.
  • Counts in outputs.apply_changes.row: events, keys, upserted, deleted and skipped_stale.

Who it's for

  • Data engineers who copy OLTP tables into a reporting database and need current state, not an append-only log.
  • Platform teams who need tables in another database without Kafka.

Why orchestrate this with Kestra

  • Debezium offsets stay in the KV store and each change file stays with its execution, so a failed batch restarts without reading the WAL again.
  • Flow concurrency keeps batches in order without a lock table.
  • Secrets keep passwords and the webhook out of the YAML.
  • The approval is a KV value and a restart, with no extra service.

Prerequisites

  • PostgreSQL 10 or later with wal_level=logical, and a user with REPLICATION and SELECT on the table. The source table needs a primary key.
  • A PostgreSQL target where the user can create the cdc schema.

Secrets

  • CDC_SOURCE_HOST, CDC_SOURCE_USER, CDC_SOURCE_PASSWORD: source database connection.
  • CDC_REPLICA_JDBC_URL, CDC_REPLICA_USER, CDC_REPLICA_PASSWORD: target database, for example jdbc:postgresql://host:5432/analytics.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for failure alerts.

Quick start

  1. Run docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16 -c wal_level=logical.
  2. Create databases shop and analytics. In shop, create public.customers (id bigint PRIMARY KEY, email text NOT NULL, full_name text, plan text NOT NULL, notes text, updated_at timestamptz NOT NULL DEFAULT now()) and insert rows.
  3. Add the secrets and save the flow. The first execution copies the snapshot within a minute.
  4. Change rows in shop, then compare with cdc.customers_live.
  5. Delete more than 500 rows to test the guard. Set KV key cdc_customers_allow_mass_delete to true and restart the execution.

How to extend

  • Another table: change includedTables, the trigger.uris key (<database>.<table>), the SQL column lists and the Batch columns.
  • Add the TOAST CASE to each large text, jsonb or bytea column, or set REPLICA IDENTITY FULL on the source table.
  • Purge tombstones older than your restart window in a scheduled flow.
  • Limits: Debezium skips TRUNCATE by default, and new source columns are not copied until you add them. If you drop the slot, also delete this flow's debezium-state KV entry.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.