Skip to content

CDC (Change Data Capture)

Stream database changes in real time via logical replication. Instead of polling, Mycel connects as a replication client and receives INSERT, UPDATE, and DELETE events the moment they happen. Use it for event sourcing, audit trails, cache invalidation, or cross-service synchronization.

Currently supports PostgreSQL (pgoutput plugin).

Configuration

connector "pg_cdc" {
  type   = "cdc"
  driver = "postgres"

  host        = "localhost"
  port        = 5432
  database    = "myapp"
  user        = "replication_user"
  password    = env("DB_PASSWORD")
  slot_name   = "mycel_slot"
  publication = "mycel_pub"
}
Option Type Default Description
driver string — Database driver (postgres)
host string "localhost" Database host
port int 5432 Database port
database string — Database name
user string — Replication user
password string — Password
slot_name string "mycel_slot" Replication slot name
publication string "mycel_pub" PostgreSQL publication name

Prerequisites: PostgreSQL must have wal_level = logical and the user must have REPLICATION privilege.

Operations

Operations use TRIGGER:TABLE format. Source only — CDC does not support write operations.

Operation Direction Description
INSERT:table source New row inserted
UPDATE:table source Row updated
DELETE:table source Row deleted
*:table source Any change on a table
INSERT:* source Inserts on any table
*:* source All changes on all tables

The flow handler receives: input.trigger, input.table, input.schema, input.new (new row for INSERT/UPDATE), input.old (old row for UPDATE/DELETE), and input.timestamp.

input.old carries only the key columns unless the table is set to ALTER TABLE <name> REPLICA IDENTITY FULL. Without it, a flow comparing what a row was against what it now is sees only the id.

Delivery

The replication slot is what makes this different from polling: PostgreSQL keeps the WAL a slot has not confirmed, so changes written while Mycel is down are still there when it reconnects, and streaming resumes from the slot rather than from the present.

That resumption is at-least-once. How far a consumer has got is reported to the server periodically, so a change already handled but not yet confirmed when the connection dropped is delivered again. A flow that must not act twice on the same change says so with a dedupe block keyed on something stable in the row.

The other side of a slot is that it costs disk. PostgreSQL cannot recycle WAL a slot still needs, so a slot belonging to a service that is never coming back grows the database's disk until it is dropped:

SELECT slot_name, active, pg_size_pretty(
  pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;

SELECT pg_drop_replication_slot('mycel_slot');

Example

# React to new user inserts
flow "on_user_created" {
  from {
    connector = "pg_cdc"
    operation = "INSERT:users"
  }
  transform {
    event = "'user.created'"
    data  = "input.new"
  }
  to {
    connector = "events_db"
    target    = "events"
  }
}

# Track order status changes
flow "on_order_updated" {
  from {
    connector = "pg_cdc"
    operation = "UPDATE:orders"
  }
  transform {
    event  = "'order.updated'"
    before = "input.old"
    after  = "input.new"
  }
  to {
    connector = "rabbit"
    operation = "PUBLISH"
    target    = "order.events"
  }
}

# Monitor all changes on a table
flow "audit_products" {
  from {
    connector = "pg_cdc"
    operation = "*:products"
  }
  to {
    connector = "audit_db"
    target    = "change_log"
  }
}

See the cdc example for a complete working setup.


Full configuration reference: See CDC in the Configuration Reference.