Open source · Apache-2.0 / MIT · Rust 1.97.0+

Change data capture
built in Rust

Stream every INSERT, UPDATE and DELETE from PostgreSQL, MySQL, MariaDB and SQL Server into Kafka, Iceberg, Snowflake, Databricks, HTTP or files — with delivery contracts that say exactly what they guarantee, a sandboxed WASM transform pipeline, and a signed audit trail.

Run it as a server, or embed it as a Rust library. Same runtime, same version, one repository.

  • No JVM
  • No Kafka Connect
  • Single binary
  • Distroless image

Two ways to run it

Both are built and tested from the same commit, against each other, at one version. The server is the library plus configuration, sinks, state backends and an operational surface — not a second implementation.

rustcdc-server

You want change data capture running.

A configured binary and distroless container image. TOML in; Kafka, Iceberg, Snowflake, Databricks, HTTP or files out. Admin API, Prometheus, OTLP traces, dead-letter queue, signed audit trail.

docker run ghcr.io/hupe1980/rustcdc-server:latest run --config-file cdc.toml
Server quickstart →

rustcdc

You want it inside something you are building.

An ordinary crate that links into your binary and runs on your Tokio runtime. You own the poll loop, the sink and the process. No sidecar to supervise, no control plane to operate.

cargo add rustcdc --features postgres
Embedding guide →

One binary, end to end

Sources

  • PostgreSQL logical replication
  • MySQL binlog + GTID
  • MariaDB binlog + GTID
  • SQL Server CDC tables

Pipeline

  • Masking redact · HMAC · AES-GCM
  • WASM transforms any language
  • Routing table → sink
  • Outbox transactional

Sinks

  • Apache Kafka idempotent · transactional
  • Apache Iceberg REST catalog
  • Snowflake Snowpipe Streaming
  • Databricks Zerobus Ingest
  • HTTP batched + retry
  • Files rotating JSONL

Built for people who get paged

Every guarantee below is either enforced by a test or documented as a limitation. Nothing is claimed twice.

Records and checkpoint commit together

With effectively_once, a batch's records and its checkpoint commit together — in one Kafka transaction, or by a Snowflake channel's offset token. Any other sink is rejected at startup for that contract rather than silently degraded.

Delivery contracts →

Failures classified on two axes

A record the broker will reject forever is quarantined. A revoked ACL halts the pipeline instead of draining your change stream into the dead-letter queue one event at a time. Conflating those is how a DLQ becomes the data loss it exists to prevent.

Dead-letter queue →

Non-blocking backfill

DBLog watermark incremental snapshots interleave with the live stream and resume mid-chunk after a restart. No held replication slot, no re-read from row zero.

Snapshots →

Sandboxed WASM transforms

Run your own logic in-process with epoch interruption, fuel metering and no host imports. Write it in Rust, AssemblyScript, or anything that targets WASM. A masking rule that never matches is a metric, not a silent no-op.

Writing transforms →

Partial-image safety

PostgreSQL unchanged-TOAST holes are tracked per image and survive transforms, sinks and the Iceberg schema. Absent is never quietly turned into NULL.

Partial images →

Observability that resolves

Prometheus and OTLP, and a health verdict with a stable stall cause that tells a quiet database from a dead socket. Shipped alert rules are tested against real metric names.

Health verdict →

Security by default

TLS is mandatory on any non-loopback bind. Kafka SASL including OAUTHBEARER and AWS MSK IAM, mTLS with hot certificate reload, Ed25519-signed audit trail, and source passwords that the config loader refuses to accept as literals.

Security →

Pluggable state

Checkpoint to the local filesystem, a Kafka topic, Redis or PostgreSQL. Network-shared backends are fenced — a Kafka topic by producer epoch, Redis and Postgres by a heartbeat lease — so two writers cannot move the durable position backwards.

State backends →

A whole pipeline is one file

TOML with a declared api_version. Unknown keys are rejected at load rather than ignored, so a typo is a startup error instead of a setting that silently never applied.

  • Validate before you deploy: rustcdc validate-config
  • See real events without writing any: rustcdc dry-run
  • Secrets by reference, never literals in the file
Configuration reference
cdc.toml
api_version       = "v1"
delivery_contract = "at_least_once"

[source.postgres]
host                  = "db.internal"
port                  = 5432
user                  = "cdc_user"
password              = { env = "POSTGRES_PASSWORD" }   # a literal here is rejected at load
database              = "app"
publication_name      = "cdc_pub"
replication_slot_name = "cdc_slot"
table_include_list    = ["public.orders", "public.customers"]
table_exclude_list    = []
conn_timeout_secs     = 10
max_events_per_poll   = 1000
stream_poll_interval_ms = 100

[source.postgres.transport]
mode = "tls"

[sink]
type    = "kafka"
brokers = "broker1:9092,broker2:9092"
topic   = "cdc.${schema}.${table}"   # one topic per table, resolved per event

# Mask before the data ever leaves the process.
[[pipeline.transforms]]
name = "redact_pii"

  [[pipeline.transforms.actions]]
  type = "mask"

    [pipeline.transforms.actions.rules]
    email = { type = "hmac_sha256", key = { env = "PII_HMAC_KEY" } }

[state.backend.kafka_topic]
brokers            = "broker1:9092,broker2:9092"
topic              = "__rustcdc_state"
durability_profile = "production"

[admin]
bind            = "127.0.0.1:8080"
probe_auth_mode = "allow_unauthenticated_loopback"

Stream your first change in ten minutes

A self-contained Docker Compose demo runs a Postgres pipeline with no configuration at all — or start from an empty Rust project and own the loop yourself.