pg-cdc — Features
Comprehensive index of what pg-cdc ships today. Outcome-first: each entry leads with what the feature does for an operator or consumer, followed by a checklist of the concrete sub-capabilities that back it up.
What pg-cdc is
A single-binary PostgreSQL change-data-capture daemon that streams WAL into typed Parquet files on cloud storage. Pure Go, no CGO, no Kafka, no JVM. Produces an immutable, governed data zone that downstream consumers (Athena, Spark, Iceberg-aware engines, AI agents via the MCP server) read from without ever touching the source database.
How to read this page
- [x] — shipped and on
main. Linked to deeper docs where useful. - [ ] — planned or in flight (rare on this page; see
docs/internal/plans/for roadmap). - (commercial) — present in this repo (the full edition) but withheld from the public OSS mirror at
github.com/burnside-project/pg-cdc. There is no in-binary edition gate: the single Apache-2.0 binary built from this repo contains every feature, and the OSS/commercial boundary is enforced by which packages are mirrored to the public repo — not by build tags or a license check (the code has neither). The mirror holds back the governance / ACL / audit / break-glass plane as the commercial moat. The authoritative, current package-by-package split lives ininternal/plans/oss-adoption.md;commercial-edition.mdon the mirror is the customer-facing summary.
1. CDC Engine
Postgres logical replication
Reads WAL from a publication + replication slot, decodes pgoutput v2 messages, emits per-row change events. The canonical Postgres replica pattern; no triggers, no application changes.
- Publication + slot lifecycle managed by
pg-cdc init/teardown -
pgoutputv2 decoder for INSERT / UPDATE / DELETE - Replica-identity validation at init (fail-loud on
REPLICA IDENTITY NOTHING) - Source LSN tracked in manifest as the consistent starting point
Managed sources — AWS RDS & Aurora
First-class support for managed Postgres alongside self-managed, behind the same pgoutput decode path. The core engine is unchanged; managed behavior is capability-driven. Full guide: 14-managed-postgres.md.
- Runtime provider detection (
source.postgres.provider: auto|self-managed|rds|aurora), logged at startup -
pg-cdc preflight— readiness check (wal_level, replication privilege, CREATE-on-database, slot headroom, replica identity) with provider-specific remediation - Built-in RDS/Aurora IAM-token auth (
iam_auth: true) — mints + rotates tokens per connect, no static password - Slot-safety guardrail (
guardrail.max_retained_wal_bytes) — alarms before a lagging slot pins enough WAL to threaten the source DB - Idle-source heartbeat (
replication.heartbeat_interval_sec) for Aurora Serverless v2 - Ephemeral-RDS/Aurora Terraform validation harness with zero-dormant-cost backstops — standalone
burnside-project-pg-cdc-testbed
Initial consistent snapshot
Captures every active table’s contents at a single LSN before streaming begins, so consumers see a clean base + replayable deltas.
-
pg-cdc initexports a snapshot viaCREATE_REPLICATION_SLOT - Per-table COPY runs against
SET TRANSACTION SNAPSHOTfor consistency - Cross-table parallel snapshot via
init.parallel_workers(default 4) — Phase 8 #4a - Iceberg
CreateTableregistered inline alongside parquet base files
Streaming WAL → delta Parquet
Long-running daemon that consumes the replication slot, buffers events, and flushes typed Parquet per table on interval or row/byte count.
-
pg-cdc startdaemon mode (one binary, no external scheduler) - Configurable flush triggers:
flush.interval_sec,flush.max_rows,flush.max_bytes - Per-table parallel flush via
flush.parallel_workers(default 4) — Phase 8 #3a - Per-table iceberg mutex serializes AppendRows / EvolveSchema / ExpireSnapshots
- Detailed phase docs in
04-streaming.md
Compaction
Merges delta epochs into a new base snapshot, applies I/U/D semantics, expires soft-deletes on a configurable TTL.
-
pg-cdc compactsubcommand (also wired to a systemd timer — see Operability) - Tombstone TTL (default 30 days, configurable)
- Iceberg snapshot expiration (default retain 100 per table)
- Manifest CAS-safe coexistence with the streaming daemon
- Compaction details in
05-compaction.md
Schema evolution
Detects new columns in source tables and propagates the new schema to the manifest, Glue, and Iceberg without restart.
- Per-flush detection of
ALTER TABLE ADD COLUMNevents - Background reconciler for drops that don’t fire a flush trigger (60 s default)
-
onSchemaEvolvedhook updates the catalog after the manifest write succeeds -
pg-cdc reconcile --table foo.bar --forceone-shot operator command
Manifest format
Versioned JSON file at the root of the sink that describes every active table, its schema version, epoch markers, and source LSN. The single source of truth across all sinks.
- Conditional writes (If-Match ETag CAS) on S3 (the only sink implementing
ConditionalManifestSink; filesystem falls back to plain writes, GCS is an unimplemented stub) - Schema-merge CAS recovery handles concurrent writers (streamer + compact)
- CAS-conflict circuit breaker fails fast on suspected dual-writer scenarios
- Branch / tag / promote refs (
refs.json) for git-shaped consumer workflows
2. Storage Sinks
Filesystem sink
Writes parquet to a local directory. Useful for dev, on-prem, and air-gapped environments.
-
storage.type: filesystem - No external dependencies; just a writable path
S3 sink
Writes to AWS S3 (or any S3-compatible store via custom endpoint — MinIO, R2, etc.).
-
storage.type: s3withbucket/prefix/region/endpoint - Multipart upload via aws-sdk-go-v2 transfer manager
- Native AWS SDK retry middleware (configurable via
aws.max_retries) - Sink-side errors increment
pgcdc_sink_errors_total(alertable)
GCS sink — not yet implemented
A GCS sink adapter is scaffolded but not implemented. The adapter under internal/adapters/sink/gcs/ returns gcs sink: not yet implemented for every operation. Do not configure storage.type: gcs for a real deployment.
-
storage.type: gcs— stub only (no ETA) - Multipart + retry behavior (deferred until the adapter is built)
3. Catalog Integrations
AWS Glue
Registers active tables in Glue so Athena, Spark, and other AWS-aware engines can query the parquet files directly.
-
catalog.type: gluewithdatabase+region - Registration runs at the end of
pg-cdc initand after every successful schema evolution - Idempotent
pg-cdc catalog registerfor re-syncing after manifest changes - Failures tracked via
pgcdc_catalog_register_failures_total+pgcdc_catalog_register_retries_total
Apache Iceberg
First-class Iceberg table writer — every flush produces an Iceberg snapshot; schema evolution and snapshot expiration handled in-tree.
-
catalog.type: icebergwithwarehouse+database - Background snapshot expiration goroutine (configurable retain count, default 100)
- Per-table mutex serializes AppendRows / EvolveSchema / ExpireSnapshots
-
pgcdc_iceberg_snapshot_countandpgcdc_iceberg_snapshots_expired_totalmetrics
4. Reliability & Hardening
Credential re-resolution on reconnect
The pgxpool’s BeforeConnect hook re-resolves ${VAR} references in the connection URL on every physical connection, so short-lived secrets (RDS IAM 15-min token, Vault leases) don’t require a daemon restart on expiry.
-
pgxpool.Config.BeforeConnectintegration (Phase 8 #1) - Covers both pool connections and the dedicated replication socket
- Operational caveats documented in
08-authentication.md
Byte-budgeted batches
A flush.max_bytes ceiling (default 128 MiB) sits alongside max_rows and the interval timer — protects against OOM under wide-row workloads (TOAST, large JSONB) where one or two rows can blow past the row counter.
- Configurable via
flush.max_bytes(Phase 8 #2) - Best-effort per-event byte accounting in the streaming hot path
- Default sized below typical container limits while leaving steady-state on
max_rows/ interval
Graceful shutdown / SIGTERM drain
On SIGTERM the streamer drains the in-flight event buffer to the sink before exit; no parquet partial files, no orphan epochs.
-
signal.NotifyContextintegration inpg-cdc start - Drain uses a fresh
context.WithTimeoutso the sink can finish even after the streaming ctx fires - Configurable
flush.drain_timeout_sec(default 30 s) - Validated by
deploy/test-harness/sigterm-loop.sh(100-cycle harness)
AWS SDK retry middleware
Native aws-sdk-go-v2 retry middleware wired on every AWS client (S3, Glue, Lake Formation, DynamoDB). Transient 5xx and throttling errors are retried with exponential backoff.
- Configurable via
aws.max_retries(default 5) - Shared
internal/awsutilhelper applies the policy to every client
PG transient-error retry with backoff
Replication-side connection drops (TCP reset, network timeout, libpq transient) trigger a bounded exponential backoff before reconnecting to the slot.
- 1 s → 30 s cap, 10 attempts default
- Transient vs fatal classification in
internal/postgres/errors.go - Exhausted retries return non-zero exit so systemd
Restart=on-failurekicks in
TOAST partial-row preservation
Postgres’ logical replication uses TupleDataType 'u' to signal that a TOAST column was unchanged by an UPDATE. pg-cdc preserves the existing value instead of writing NULL — a silent data-corruption fix.
- Decoder emits
pkg/parquet.UnchangedTOASTSentinelfor'u'columns -
MergeDeltasin compact honors the sentinel by preserving the base value
Manifest CAS recovery + circuit breaker
Concurrent writers (streamer + scheduled compact) coordinate via conditional writes on the manifest. On conflict, both retry with a Schemas-aware merge instead of blindly overwriting.
- If-Match ETag CAS on S3 (filesystem falls back to plain writes; GCS is an unimplemented stub)
-
Schemasfield merged by max version per table on conflict -
pgcdc_manifest_conflicts_resolved_totalcounter tracks healthy conflict rate - Circuit breaker trips when N conflicts within W seconds — points at dual-writer misconfig (default 10/60)
Backpressure observability
When the source-adapter bridge channel is full, the streamer surfaces the wait time so operators can spot a slow sink before it OOMs the daemon.
-
pgcdc_event_channel_full_totalandpgcdc_event_channel_blocked_seconds_total - Channel cap warning log at 10 000 buffered events
5. Observability
Prometheus metrics
The daemon exposes 29 metrics on :9090/metrics covering replication health, throughput, sink errors, buffer pressure, compaction, catalog registration, Iceberg snapshots, schema reconciliation, CAS conflicts, backpressure, and the MCP/REST audit surface.
- Replication:
pgcdc_replication_lag_seconds,pgcdc_slot_active - Slot safety (managed):
pgcdc_slot_retained_wal_bytes,pgcdc_slot_guardrail_breach,pgcdc_heartbeat_total - Throughput:
pgcdc_events_per_sec,pgcdc_buffer_depth,pgcdc_flush_parallel_workers_used - Sink:
pgcdc_sink_latency_seconds(histogram),pgcdc_sink_errors_total - Source-side:
pgcdc_source_oldest_xact_age_seconds(Phase 8 #5) - Compaction:
pgcdc_compact_duration_seconds,pgcdc_compact_staleness_seconds - Catalog:
pgcdc_catalog_register_failures_total,pgcdc_catalog_register_retries_total - Iceberg:
pgcdc_iceberg_snapshot_count,pgcdc_iceberg_snapshots_expired_total - CAS coordination:
pgcdc_manifest_conflicts_resolved_total - Backpressure:
pgcdc_event_channel_full_total,pgcdc_event_channel_blocked_seconds_total - Schema reconcile:
pgcdc_reconcile_staleness_seconds - ACL:
pgcdc_acl_drift_events_total,pgcdc_acl_sync_staleness_seconds - Per-table progress:
pgcdc_table_latest_epoch,pgcdc_events_flushed_total; refs retention:pgcdc_refs_raw_tags_pruned_total - MCP/REST audit surface:
pgcdc_audit_events_total,pgcdc_audit_duration_ms,pgcdc_audit_dropped_total - Full table with alert shapes in
observability/metrics.md
CloudWatch dashboard + alarms
Terraform-managed dashboard (11 panels) and alarm set per deployment. Parameterized by deployment_name; zero per-deployment customization required.
-
deploy/terraform/governance/observability.tf+templates/dashboard.json.tpl - 9 alarms covering replication lag, sink errors, compact staleness, ACL drift, buffer depth, reconcile staleness, source xact age, manifest conflicts, event-channel block
Agent-consumable run artifacts
Per-run directory under <state-dir>/runs/<run_id>/ containing structured JSON events, a state snapshot, run metadata, and a shutdown summary. Designed for LLM agents to crawl without bespoke parsers.
-
run.json— config + start/stop metadata -
events.ndjson— structured event stream with stable schema version -
state.json— minute-cadence snapshot of LSN, lag, per-table epoch, buffer depth, last error -
summary.md— written on shutdown (event counts, schema evolutions, compacts, ACL syncs) - Schema doc in
observability/event-schema.md
RefreshLag timeout (Phase 9 #7)
Bounded context.WithTimeout on the periodic metric-refresh queries so a saturated pgxpool can’t wedge the state-writer goroutine and freeze the gauges.
- Default 10 s timeout, overridable via
runtimeState.refreshTimeout - Stuck queries logged + abandoned; next tick retries
- Validated live on
daedmonds4— gauge now produces distinct values under load (pre-fix: frozen to 14 decimal places)
6. Governance & ACL (commercial)
Layer-2 tag governance — (commercial)
Policy-as-code in the governance: block of pg-cdc.yml. Required-tag enforcement, pattern-matched overrides, column-level tags, emergency overrides with expiry.
- Strict mode refuses writes for resources missing required tags
- JSON Schema validation at config-load time (fail-loud on misconfig)
- Tag values validated against Layer-1 LF-Tag taxonomy via
lakeformation:GetLFTagbefore DynamoDB write
DynamoDB ACL registry — (commercial)
Versioned classification store: one item per resource (db.schema.table), monotonic version, direct + resolved tags, audit fields (last_intent_at, last_intent_by, last_intent_reason).
-
pg-cdc acl register | get | set | list | diff | syncsubcommand tree - Sync exit codes designed for CI: 0 = clean, 3 = drift healed, 2 = AWS error
-
last_applied_epochmarker written back to DDB after each sync
AWS Lake Formation reconciliation — (commercial)
pg-cdc acl diff computes the add/remove plan vs. live LF tags; pg-cdc acl sync applies it through AddLFTagsToResource / RemoveLFTagsFromResource.
- Idempotent re-runs (LF Add is value-idempotent)
- Every plan item emits an audit event before apply
Break-glass workflow — (commercial)
.github/workflows/break-glass.yml for time-boxed emergency grants. Input validation, GitHub Environment approval gate, automatic TTL revocation.
- Aggressive CloudWatch audit trail (grant + revoke phases)
- Per-deployment + break-glass log streams in
/pgcdc/auditlog group - OIDC-trusted IAM role with
lakeformation:Grant/RevokePermissions
Operator audit aggregation — (commercial)
Single CloudWatch log group across deployments with 365-day retention; per-deployment + break-glass streams; IAM write grants for the daemon role.
-
deploy/terraform/governance/audit_logs.tf(/pgcdc/auditlog group)
7. Consumption Layer
OSS / commercial split: the MCP server is the OSS consumption path. The REST API server, manifest refs (
refstore/— branch/tag/promote), and the rate limiter (ratelimit/) are currently held back from the public mirror perinternal/plans/oss-adoption.mdand are therefore (commercial). As with everything on this page, they are fully present in this repo’s binary — see the legend at the top for what “(commercial)” means.
MCP server
First-class Model Context Protocol server so AI agents (Claude Desktop, Cursor, etc.) can query governed Parquet directly via DuckDB. Eight tools (list_tables, describe_table, query, get_changes, get_manifest, get_freshness, get_snapshot, get_diff) — see 11-ai-agent-consumption.md.
-
pg-cdc serve --mcpexposes the MCP endpoint - Query-time Lake Formation enforcement — every read resolves the caller’s LF grants via
LFGrantChecker(5-min cache); columns the role can’t see never leave storage. Live enforcement, not manifest-based. - Manifest + refs reload on a configurable interval (default 30 s)
- Live ACL refresh —
pg-cdc acl setbumpspolicy_versionand the new surface appears without a server restart (the refresh loop re-reads policy versions) - Freshness signal (
is_stale) honors the configured SLO
REST API server
HTTP surface for non-MCP consumers — same query semantics, same governance, different transport. Two endpoint families: the data plane (spec/data-plane-api.md — /v1/tables*, /v1/query, /v1/manifest, /v1/diff) and the console/operational surface (spec/console-api.md — /v1/status, /v1/catalog, /v1/audit/summary, and SSE /v1/stream/{status,events,audit}). /metrics + /healthz always on.
- Bearer-token auth (constant-time comparison) — required for non-loopback bind
- Loopback bind by default;
--listen-addrflag for explicit network exposure - Server timeouts + body cap + bounded handler middleware (1 MiB request body)
- Rate-limiting (per principal) configurable
- Server-Sent Events streams (
/v1/stream/{status,events,audit}) for live console/dashboard updates without polling - Glue catalog drift detection —
/v1/catalogjoins the live Glue catalog against the manifest and flags per-table version drift (cached 30 s, serves stale-but-flagged on Glue errors) -
--pprof <loopback-addr>exposes/debug/pprof/for profiling (loopback only; off by default)
Manifest refs — branch / tag / promote
Git-shaped workflow for consumers: staging advances on every flush, main advances based on promotion.mode (auto or manual), immutable raw@<ts> tags every flush for time-travel.
-
refs.jsonwritten alongside the manifest after every flush -
pg-cdc promotefor manual mode operators - Tag-window time travel honored by Iceberg snapshot-retention math
8. Operability
systemd integration
Production deployment runs as a systemd unit. Companion timers for scheduled work (compaction, soak test).
-
deploy/systemd/pg-cdc.service— main daemon (User=pgcdc, security-hardened) -
deploy/systemd/pg-cdc-compact.{service,timer}— hourly compaction -
deploy/systemd/pg-cdc-e2e-soak.{service,timer}— hourly soak test (Phase 9 #5) - Drop-in overrides documented for memory caps + GOMEMLIMIT (
systemctl edit pg-cdc)
Runbooks
On-call documentation for the recurring failure classes; symptom → alert → diagnosis → remediation → prevention shape.
- daemon-oom
- replication-slot-full
- s3-5xx-spike
- compact-backlog
- acl-sync-drift — (commercial)
- glue-api-throttling
- postgres-version-upgrade
- dependency-bump-broke-build
- disaster-recovery
- upgrade (zero-loss binary swap procedure)
- reconcile-staleness
- source-oldest-xact
- manifest-conflicts
- event-channel-blocked
- All 14 under
runbooks/
SLOs
Replication lag P99 < 5 s, audit-object lag P99 < 10 min, zero silent data loss. Error budgets + incident classification.
On-call playbook
Quick reference, triage flow, escalation tree, runbook index, key file paths.
9. Testing & Validation
Integration suite (Docker)
Real Postgres + LocalStack S3 + fake Glue end-to-end pipeline test in CI.
-
internal/integration/with//go:build integrationtag -
docker-compose.integration.ymlfor local + CI runs - CI integration job in
.github/workflows/ci-cd.yml(Postgres 16 service container)
e2e-soak.sh — 5-minute end-to-end soak
Operator-runnable test that exercises the full PG → CDC → S3 → catalog pipeline against a live deployment. Eight phases, ~5 minute budget, hourly cron-friendly.
- Pre-flight → INSERT / UPDATE / DELETE → drain → compact → schema-evo → validate → cleanup
- Two-tier WARN/PASS for drain (handles standby-cadence tail without false-FAIL)
- In-line
pg_stat_activitydiagnostic on drain stall - Schema-evolution arm — ALTER ADD COLUMN + Glue verify + DROP COLUMN cleanup
- Compaction-cycle arm — runs
pg-cdc compactmid-test, verifies staleness reset - Hourly cron via
pg-cdc-e2e-soak.timer; alerts on^FAIL:in journal - Runbook at
../deploy/test-harness/e2e-soak.md
Failure-injection harness
Scripts that simulate the recurring failure modes locally so runbooks can be rehearsed without breaking prod.
-
inject-oom.sh,inject-slot-full.sh,inject-s3-failure.sh -
sigterm-loop.sh(100-cycle SIGTERM drain validation) -
load-gen.sh(env-based auth, deterministic INSERT load) - All scripts support
--self-testto validate prerequisites
10. Security & Authentication
Authentication profiles
Four supported PG auth methods; all wire through the connection URL so secret management stays at the URL boundary.
- Password (with
${VAR}env-var substitution + re-resolution on reconnect) - mTLS (client cert + key)
- AWS RDS IAM (short-lived token, refreshed via the BeforeConnect hook)
- GCP Cloud SQL IAM (via the Cloud SQL Auth Proxy sidecar)
- Operational caveats (env scoping, silent-empty footgun, CLI vs daemon env) documented in
08-authentication.md
Required Postgres privileges
Documented minimal grant set: SELECT on covered schemas, REPLICATION on the user, SELECT on information_schema privilege views for governance, optional pg_read_all_stats for full-fleet oldest-txn visibility.
Compliance overlays — (commercial)
Pre-built deployment topologies that pass common audit profiles.
- HIPAA deployment overlay (
security/hipaa-deployment.md) - Least-privilege deployment posture (
security/least-privilege-deployment.md) - B5b producer-side security audit on file (
internal/audits/security-audit-b5b.md)
Where to go next
- New to pg-cdc? Start at
01-getting-started.md. - Operating an existing deployment?
07-operations.md+runbooks/. - Looking for the OSS / commercial split? The authoritative, current boundary (package-by-package) is
internal/plans/oss-adoption.mdin this repo; the customer-facing summary iscommercial-edition.mdon the public mirror. The split is repository-level — there is no in-binary edition gate. - Planning work?
internal/plans/production-readiness.mdfor the live roadmap.