Getting started
Install rustcdc, write a minimal configuration, and stream your first row-level change from PostgreSQL, MySQL, MariaDB or SQL Server in about ten minutes.
On this page
To see it working before configuring anything, run the self-contained demo. It streams live PostgreSQL changes to your terminal in under two minutes:
cd demo && docker compose up
You need a database you can enable replication on, and either Rust or Docker. Pick the install path that matches where this will run — a binary for local development, VMs and bare metal, or the container image for Kubernetes and CI.
Install with Homebrew
On macOS (Apple Silicon) and Linux (x86_64, arm64):
brew install hupe1980/tap/rustcdc
rustcdc --version
The formula installs the release binary built with every connector and sink.
Install the binary
Requires Rust 1.97.0 or later (rustup update stable).
git clone https://github.com/hupe1980/rustcdc
cd rustcdc
cargo build --release --locked
sudo cp target/release/rustcdc /usr/local/bin/
rustcdc --version
Building from source needs cmake, clang and perl on the build host — they are
required by aws-lc-sys, the cryptography backend used for TLS.
Choosing connectors
A source build captures PostgreSQL only. Connectors are cargo features, and the
default is postgres:
| Feature | Connectors | Default |
|---|---|---|
postgres | PostgreSQL logical replication | yes |
mysql | MySQL and MariaDB binlog | no |
sqlserver | SQL Server CDC | no |
glue | AWS Glue Schema Registry codec | no |
# MySQL and MariaDB instead of PostgreSQL
cargo build --release --locked --no-default-features --features mysql
# Everything
cargo build --release --locked --all-features
At least one connector is required; a build with none fails to compile rather than producing a binary that rejects every configuration.
A connector you leave out is a driver you do not link, audit or patch. Every build links
exactly one TLS stack — one rustls, one rustls-webpki — whichever connectors it has, and a
test asserts it for the default build and for the all-features build the image ships.
A config naming a connector the binary lacks is rejected at startup with the feature to rebuild with — it never loads silently.
The container image is built with --all-features and carries every connector, so
none of this applies if you deploy the image.
Run with Docker
The official image is multi-arch (linux/amd64, linux/arm64), runs as a
non-root user, and is based on distroless/cc — no shell, no package manager.
Pull
docker pull ghcr.io/hupe1980/rustcdc-server:latestVerify
docker run --rm ghcr.io/hupe1980/rustcdc-server:latest --versionPrepare PostgreSQL
rustcdc reads from the PostgreSQL logical replication stream. You need
wal_level = logical and a replication user.
-- 1. Enable logical replication (requires superuser; needs a server restart
-- if wal_level was not already "logical")
ALTER SYSTEM SET wal_level = logical;
SELECT pg_reload_conf(); -- confirm with: SHOW wal_level;
-- 2. Create a dedicated replication user
CREATE USER cdc_user WITH REPLICATION LOGIN PASSWORD 'changeme';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user;
-- 3. Create a publication for the tables you want to capture
CREATE PUBLICATION cdc_pub FOR TABLE public.orders, public.customers;
Cloud databases: RDS, Azure Database for PostgreSQL, and Cloud SQL all support logical replication — see the PostgreSQL connector guide.
Create your config
Generate a starter file:
rustcdc init --output cdc.toml --profile dev
Then edit cdc.toml to match your environment. The minimal working config:
api_version = "v1"
[source.postgres]
host = "localhost"
port = 5432
user = "cdc_user"
password = { env = "POSTGRES_PASSWORD" } # never hardcode secrets
database = "mydb"
publication_name = "cdc_pub"
replication_slot_name = "cdc_slot"
table_include_list = ["public.orders", "public.customers"]
table_exclude_list = []
conn_timeout_secs = 10
stream_poll_interval_ms = 100
max_events_per_poll = 1000
# Quickstart convenience: create the replication slot on first connect.
# In production, provision the slot out of band and set this to false —
# a slot that vanishes mid-life is a data-loss event, and recreating it
# silently would skip everything in between.
create_replication_slot_if_missing = true
[source.postgres.transport]
mode = "plaintext"
[sink]
type = "stdout" # swap for http, kafka, or iceberg when ready
[state]
dir = "./state"
Validate before running:
rustcdc validate-config --config-file cdc.tomlFirst run
Binary
export POSTGRES_PASSWORD="changeme"
# (Kafka-topic state backend only) seed the state topic first:
# rustcdc init-state --config-file cdc.toml
# Start streaming — the slot is created on first connect thanks to
# create_replication_slot_if_missing = true
rustcdc run --config-file cdc.tomlDocker
docker run --rm \
-e POSTGRES_PASSWORD="changeme" \
-v "$PWD/cdc.toml:/etc/rustcdc/config.toml:ro" \
-v "$PWD/state:/var/lib/rustcdc/state" \
-p 8080:8080 \
ghcr.io/hupe1980/rustcdc-server:latest \
run --config-file /etc/rustcdc/config.toml
Tip: add
--network host(Linux) or configurehost.docker.internalso the container can reach your local Postgres.
Docker Compose (development)
# compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: mydb
command: ["postgres", "-c", "wal_level=logical"]
ports: ["5432:5432"]
rustcdc:
image: ghcr.io/hupe1980/rustcdc-server:latest
depends_on: [postgres]
environment:
POSTGRES_PASSWORD: changeme
volumes:
- ./cdc.toml:/etc/rustcdc/config.toml:ro
- rustcdc-state:/var/lib/rustcdc/state
ports: ["8080:8080"]
command: ["run", "--config-file", "/etc/rustcdc/config.toml"]
volumes:
rustcdc-state:docker compose upVerify it's working
Events stream to stdout as JSON lines — the full rustcdc event envelope
(abridged; real events also carry source, ts, transaction, and
envelope_version):
{"before":null,"after":{"id":42,"amount":"99.99","status":"pending"},"op":"insert",
"schema":"public","table":"orders","primary_key":["id"],"before_is_key_only":false}
Check the admin API (available on port 8080). With the dev profile, /healthz,
/livez, and /readyz need no token; /status and /metrics always require a
read token — configure [admin] read_token_env and export the variable first:
# Liveness / readiness (no token needed with the dev profile)
curl http://localhost:8080/livez
curl http://localhost:8080/readyz
# Pipeline status and Prometheus metrics (read token required)
export RUSTCDC_ADMIN_READ_TOKEN="my-dev-read-token" # matches [admin] read_token_env
curl -H "Authorization: Bearer $RUSTCDC_ADMIN_READ_TOKEN" http://localhost:8080/status
curl -H "Authorization: Bearer $RUSTCDC_ADMIN_READ_TOKEN" http://localhost:8080/metricsAdd a transform (optional)
rustcdc can modify, filter, or enrich every event before it reaches the sink. Two modes are available:
Native rules — zero-code, declared in TOML. Good for filtering and simple field operations:
# Drop audit-log events and stamp provenance metadata onto customer events
[[pipeline.transforms]]
name = "drop-audit-log"
[[pipeline.transforms.actions]]
type = "filter"
include_tables = ["customers", "orders"] # everything else is dropped
[[pipeline.transforms]]
name = "stamp-metadata"
[pipeline.transforms.when]
tables = ["customers"]
ops = ["insert", "update", "read"]
[[pipeline.transforms.actions]]
type = "metadata_projection"
target_field = "_meta"
fields = ["source_name", "offset", "table", "operation"]
Available native actions: unwrap, flatten, filter, route,
metadata_projection, key_shaping. For column redaction/masking, use a WASM
module (below).
WASM module — arbitrary logic compiled from Rust, AssemblyScript, TinyGo,
or any wasm32 target. Use this for enrichment, complex routing, custom
pseudonymisation, and anything native rules cannot express:
[pipeline.transform_runtime]
mode = "wasm"
[pipeline.transform_runtime.wasm]
module_path = "/etc/rustcdc/my_transform.wasm"
timeout_ms = 50
max_memory_bytes = 8388608
instance_pool_size = 4
→ Writing WASM transforms for build steps, patterns, testing, and Kubernetes deployment.
Production checklist
Before going to production, work through these items:
-
Sink — swap
stdoutforkafka,http, oriceberg(sink configuration) -
Kafka topic layout — one topic, or
topic = "cdc.${schema}.${table}"for one per table. Create them first; rustcdc does not (topic naming) -
Delivery contract — set
delivery_contract = "at_least_once"(default) or"effectively_once"for Kafka (delivery contracts) -
State backend — replace
local_fswithkafka_topic,redis, orpostgresqlfor HA (state backends) -
Secrets — all passwords via
{ env = "VAR" }, never inline - Audit trail — enable signing and IP pseudonymisation (security hardening)
-
Health probes — wire
/livezand/readyzto your orchestrator (admin API) -
SLO alerts — deploy
monitoring/rustcdc_slo_alerts.ymlto Prometheus
Next steps
| Guide | What it covers |
|---|---|
| PostgreSQL connector | Publications, slots, replica identity, RDS/Azure/GCP, HA failover |
| MySQL / MariaDB connector | binlog_format, server_id, grants, GTID |
| SQL Server connector | sp_cdc_enable_db, permissions, polling interval |
| Core concepts | Event model, pipeline lifecycle, delivery contracts, circuit breaker |
| Configuration reference | Every TOML field with defaults and examples |
| Operations guide | Replay, migrate-state, graceful shutdown, performance tuning |
| Writing WASM transforms | Write custom transforms in Rust, AssemblyScript, or any WASM language |