Integration · Platform engineering
How to build a change data capture pipeline
A nightly poll on a modified date misses every hard delete and every update the application makes without touching the timestamp. This is the design behind capture that does not, the prompts that build it, and what production adds.
Built with Tray Headless
- System Postgres
- Step Read the log
- Step Snapshot and stream
- Step Apply in order
- System Snowflake
The database log is the source. The snapshot and the stream overlap deliberately, because rows written during the snapshot belong to both.
The short answer
What is change data capture?
Change data capture reads the database log so every insert, update and delete is seen exactly as the database saw it, takes an initial snapshot that overlaps the stream instead of abutting it, preserves per-key ordering all the way to the target, and handles a source schema change without losing position. Where this usually goes wrong is polling a modified timestamp. That misses hard deletes entirely, misses any update the application writes without touching the column, and quietly produces a warehouse that disagrees with production.
What matters here
- Read the log, not a timestamp column. Polling misses hard deletes and any update that does not touch the column.
- Overlap the snapshot and the stream. Rows written during the snapshot belong to both, and abutting them loses those rows.
- Order matters per key, not globally. Two updates to one row arriving backwards leave the wrong value in place.
- A delete is an event. Dropping it turns the target into a store of everything that ever existed.
- Checkpoint the log position after the write lands, never before.
- A source schema change is the most common way a pipeline stops. Detect it and pause rather than silently dropping the column.
Who this is for
You run data or platform engineering. The warehouse is loaded by nightly queries, it disagrees with production often enough that people check both, and nobody trusts a row count.
How it works in practice
The path from a row changing in production to that change being true downstream.
- 1
The connector reads the database log
The write-ahead log or binlog, which is the only place a hard delete appears.
- 2
An initial snapshot runs alongside the stream
Overlapping, so rows written during the snapshot are captured by one or the other.
- 3
Each change becomes an event with its key
Operation, key, before and after images, and the log position it came from.
- 4
Events are applied in per-key order
Two updates to one row must land in the order the database made them.
- 5
Position is checkpointed after the write
So a crash replays rather than skips.
- 6
A schema change pauses rather than drops
Because a silently dropped column is discovered a month later.
What the pipeline is made of
Four parts. The second is the one that quietly loses rows.
Log-based capture
The write-ahead log or binlog. A timestamp poll cannot see a hard delete, and cannot see an update that left the column alone.
An overlapping snapshot
The initial load and the stream must overlap. Rows written while the snapshot is running are exactly the rows a clean handover loses.
Per-key ordering
Two updates to one row arriving out of order leave the wrong value sitting there, and nothing will ever correct it.
Position checkpointing
Committed after the target write lands. Checkpointing first turns any crash into silent data loss.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up and read the log
A timestamp column cannot see a hard delete.
Headless skills
build-workflowtray-patternsUse build-workflow. The systems in play are Postgres and Snowflake, or whatever we run in those seats. Capture changes from the write-ahead log, not from a query over a modified timestamp. For each change record the operation, the primary key, the before and after images, the transaction identifier and the log position. The before image matters more than it looks. Without it you cannot tell which fields actually changed, and downstream consumers end up reprocessing whole rows because the event does not say what moved. Confirm the source is configured for logical replication and that the replication slot has a retention policy. An unconsumed slot holding log files is how a capture pipeline takes the production database down, and it is the failure that gets this pattern banned in an organisation.
Monitor replication slot lag from the first day. A stalled consumer makes the source database retain log files until the disk fills, so the pipeline failure becomes a production incident.
- 2
Overlap the snapshot with the stream
Rows written during the snapshot belong to both.
Headless skills
build-workflowUse build-workflow. Start streaming before the snapshot, and keep streaming throughout: Note the current log position Begin buffering the stream from that position Take the snapshot of existing rows Apply the buffered stream on top, keyed, so a row that changed during the snapshot ends at its latest value Never take a snapshot and then start the stream from afterwards. Every row written in the gap is lost, silently, and shows up months later as a handful of records that were correct in production and never arrived. Make the snapshot resumable. A snapshot of a large table that fails at 80% and restarts from zero is how this stalls for a week.
- 3
Preserve order per key, and treat a delete as an event
Two updates arriving backwards leave the wrong value in place.
Preserve ordering per primary key. Global ordering is not required and is expensive; per-key ordering is required and is not optional. If events are partitioned, partition by primary key so every change to one row travels the same path. Handle deletes explicitly: A hard delete is an event with the key and the before image Prefer a soft delete in the target: mark the row deleted with a timestamp instead of removing it Never silently drop a delete event, which turns the target into a store of everything that has ever existed Handle a primary key update as a delete followed by an insert, because that is what it is, and treating it as an update leaves an orphan behind under the old key.
- 4
Checkpoint after the write, and survive a schema change
Checkpointing first turns a crash into silent data loss.
Headless skills
tray-gotchastray-patternsUse tray-gotchas, then get the failure behaviour right. Commit the log position only after the target write has landed. On a crash this replays some events, so make the target write idempotent, keyed on primary key and log position. Replaying is cheap and skipping is not recoverable. On a source schema change: A new column is added to the target and backfilled as null A dropped column is retained in the target with a note, never dropped in step A type change pauses the pipeline and alerts, because coercing silently is how a numeric column becomes text downstream A new table is not captured until somebody says it should be Send anything unprocessable to a dead letter store with the full event, and alert on the first one. A dead letter queue nobody watches is a data loss mechanism with extra steps.
- 5
Reconcile, because a pipeline that looks healthy can still be wrong
Lag is not correctness.
Headless skills
tray-patternsReconcile periodically rather than trusting the stream: Row counts per table, source against target Checksums over a sample of keys, which catches a value that drifted without the count changing The newest log position consumed against the newest produced, which is the real lag Report: lag in seconds and in events, replication slot size, dead letter volume, snapshot progress, and reconciliation differences by table. Replication slot size is the one that becomes an incident. Everything else degrades; that one fills a disk on the source database.
- 6
Test it, then hand the table list to data engineering
Because tables get added and schemas move.
Run the per-step schema checks and the whole-workflow audit before this touches production. Delete a row in a sandbox source and confirm it arrives as a delete event. Kill the pipeline mid-snapshot and confirm it resumes without duplicating or losing rows. Then open the same workflow in Tray Build so data engineering can add tables, set the soft-delete behaviour and adjust the reconciliation cadence in the visual canvas.
What it connects to
Capture happens at the source log and lands in the warehouse, with somewhere to put what cannot be processed.
Postgres
Read the write-ahead log through a replication slot, which is the only view that includes hard deletes.
Reads
Snowflake
Apply changes keyed and idempotent, holding both current state and the change history.
Writes
Snowflake
Hold the dead letter store and the reconciliation results, so a difference has somewhere to be investigated.
Reads and writes
Datadog
Track lag, replication slot size and dead letter volume, because slot size is the one that ends as an incident.
Writes
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against Google BigQuery, Microsoft Teams, Databricks, Google Chat or AWS Redshift.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
This is how production data becomes analytical data. It runs continuously or the target is simply wrong.
It runs where production runs, not on a laptop
A capture pipeline is not a job. It holds a log position, and a process that stops holding it loses the thread entirely.
Replication slot health is watched
An unconsumed slot makes the source retain log files. Left alone it fills a disk, and the pipeline failure becomes a database incident.
Credentials are held by the platform, never hardcoded
Replication access to the source and write access to the warehouse, both scoped narrowly and held in your workspace.
Data engineering own the table list
Which tables are captured, how deletes are represented downstream and where the reconciliation thresholds sit, all open in Tray Build.
Reconciliation runs independently
Because a pipeline reporting zero lag can still have applied an event out of order, and only a checksum finds that.
Questions people ask
Why not poll a modified timestamp?
Because polling cannot see a hard delete, and cannot see an update the application writes without touching that column. Both produce a target that quietly disagrees with production.
Why must the snapshot overlap the stream?
Because rows written while the snapshot runs belong to both. Taking a snapshot and starting the stream afterwards loses everything written in the gap, silently.
Does ordering have to be global?
No, and enforcing it is expensive. Per-key ordering is what matters: two updates to one row arriving backwards leave the wrong value with nothing to correct it.
What should happen on a source schema change?
A new column is added and backfilled null, a dropped column is retained, and a type change pauses the pipeline. Coercing a type silently is how a numeric column becomes text downstream.
What is the failure that becomes a production incident?
An unconsumed replication slot. The source database retains log files for a consumer that is not reading, and eventually fills a disk.
Related guides
Data operations
How to build a CRM to warehouse sync
Capture history instead of current state, handle deletes and field changes, land raw then model, and prove the row counts. The Headless prompts that build it.
Data operations
How to build schema change management
Detect a source change before it breaks a model, resolve what depends on it, and tell the owner in time to act. The Headless prompts that build it.
Data operations
How to build reverse ETL from the warehouse
Push modelled data back into the tools people work in, syncing deltas, respecting field ownership, and never overwriting a human. The prompts that build it.
Platform engineering
How to build an API facade
Give consumers one contract instead of six systems, absorb upstream changes rather than leaking them, and keep the caller identity intact. The prompts.
Last reviewed September 2026.