PostgreSQL Source

Prerequisites, permissions, and full configuration reference for the Nanosync PostgreSQL source connector.

Nanosync replicates from PostgreSQL using logical replication with the built-in pgoutput plugin — no third-party extensions (wal2json, decoderbufs) are required. Each pipeline gets its own replication slot and publication, so multiple pipelines on the same database do not interfere.

Prerequisites

Setup

  1. Enable logical replication

    Add the following to postgresql.conf and restart PostgreSQL:

    wal_level = logical
    max_replication_slots = 10   # one slot per pipeline; ensure headroom
    max_wal_senders = 10

    Verify:

    SHOW wal_level;   -- must return "logical"
  2. Create the replication user

    CREATE ROLE nanosync LOGIN REPLICATION PASSWORD 'secret';

    Grant SELECT on specific tables:

    GRANT SELECT ON TABLE public.orders, public.order_items TO nanosync;

    Or grant access to an entire schema (including future tables):

    GRANT SELECT ON ALL TABLES IN SCHEMA public TO nanosync;
    ALTER DEFAULT PRIVILEGES IN SCHEMA public
      GRANT SELECT ON TABLES TO nanosync;

    The REPLICATION attribute allows the user to open replication connections. It does not grant superuser access.

  3. Replication slot and publication

    Nanosync creates both automatically on first connect using names derived from the pipeline name (nanosync_slot_<pipeline> and nanosync_pub_<pipeline>). To create them manually:

    SELECT pg_create_logical_replication_slot('nanosync_slot_orders', 'pgoutput');
    
    CREATE PUBLICATION nanosync_pub_orders
      FOR TABLE public.orders, public.order_items;
    -- or for all tables:
    CREATE PUBLICATION nanosync_pub_orders FOR ALL TABLES;
  4. (Optional) Set REPLICA IDENTITY for DELETE support

    By default, PostgreSQL only includes the primary key in DELETE events. To include the full row:

    ALTER TABLE public.orders REPLICA IDENTITY FULL;

    Tables without a primary key must have REPLICA IDENTITY FULL set for DELETE events to be captured.

  5. (Optional) Failover slots — PostgreSQL 17+

    Failover slots are synchronised to physical standbys so the slot position survives a primary promotion without data loss. Enable with failover_slots: "true" in the pipeline properties (see below).

Connection configuration

connections:
  - name: prod-postgres
    type: postgres
    dsn: "postgres://nanosync:${env:PG_PASSWORD}@db.prod:5432/mydb?sslmode=require"

DSN format:

postgres://[user]:[password]@[host]:[port]/[database]?[options]
DSN optionExampleDescription
sslmoderequireTLS mode: disable, require, verify-ca, verify-full
sslrootcert/etc/ssl/ca.crtCA certificate path (needed for verify-ca / verify-full)
connect_timeout10Connection timeout in seconds
application_namenanosyncShown in pg_stat_activity

Pipeline configuration

pipelines:
  - name: orders-pipeline
    source:
      connection: prod-postgres
      tables:
        - public.orders
        - public.order_items
      properties:
        replication_slot: nanosync_slot_orders   # created automatically if absent
        publication:      nanosync_pub_orders    # created automatically if absent
        snapshot_workers: "4"
        chunk_size:       "10000"

Source properties

Connection

PropertyDefaultDescription
replication_slotnanosync_slot_<pipeline>Logical replication slot name. Created with pgoutput if it does not exist.
publicationnanosync_pub_<pipeline>Publication name. Created for the configured table set if it does not exist.

Snapshot

PropertyDefaultDescription
snapshot_workers4Goroutines used to stream CTID partitions concurrently during the initial backfill. Each worker opens an additional database connection. Clamped to 1 on PostgreSQL < 14.
chunk_size10000Rows fetched per keyset-pagination page during backfill.
partition_strategyctidHow the snapshot divides tables into parallel chunks. Options: ctid, ntile, minmax.
partition_columnColumn for ntile or minmax strategies. Required when partition_strategy is not ctid.
ctid_partition_blocks1000Heap blocks (~8 MB) per CTID partition. Only used with ctid strategy.

Partition strategy details:

StrategyBest for
ctid (default)Most tables. Divides by physical page range — fast and no table stats required.
ntileHigh-bloat tables. Balances by row count using a window function — equal rows per worker even with many dead tuples.
minmaxUniformly distributed numeric or timestamp columns. Divides by value range — requires partition_column.

CDC / streaming

PropertyDefaultDescription
wal_heartbeat_enabledtrueEmit a periodic WAL message to advance confirmed_flush_lsn on idle sources. Prevents WAL accumulation when there are no changes.
wal_heartbeat_interval10sPeriod between WAL heartbeat messages.
wal_sender_timeout120sSet on each replication connection. Controls how long the server waits for a standby status update before closing the connection.
keepalive_idle_interval10sPeriod between periodic standby status updates when no new WAL events have arrived. Lower values keep slot lag smaller at the cost of network traffic.
slot_lag_alert_mb5000Log a warning when the replication slot lag exceeds this many MB. Set to 0 to disable.
failover_slotsfalseCreate the slot with failover = true (PostgreSQL 17+). Synchronises the slot to physical standbys so it survives a primary promotion.
batch_boundary_signalingfalseBuffer events within each source transaction and deliver them atomically on COMMIT. Allows the sink to commit once per source transaction instead of once per event.
max_reconnect_attempts5Maximum consecutive reconnect retries before the CDC goroutine gives up and the pipeline errors.

Tuning

PropertyDefaultDescription
cdc_channel_buffer_size131072Capacity of the internal event channel. Larger values reduce the chance of the receive loop blocking and triggering a WAL sender timeout.
normalize_buffer_hours24Maximum hours of unprocessed batches allowed in the normalizer queue before the pipeline scheduler pauses to let it catch up.

How replication works

  1. Snapshot — On first start, Nanosync performs a consistent snapshot of each table using snapshot_workers parallel goroutines. Each worker reads rows in chunk_size pages via keyset pagination, partitioned by the chosen partition_strategy.

  2. CDC — After the snapshot, Nanosync subscribes to the replication slot and streams WAL events using pgoutput. Each event is decoded into an Apache Arrow record batch with operation type (INSERT / UPDATE / DELETE), the before row, and the after row.

  3. Checkpoint — The acknowledged LSN is persisted to the state store after each batch is committed by the sink. On restart, replication resumes from the last checkpoint with no data loss.

Supported operations

OperationSupportedNotes
INSERTYes
UPDATEYesIncludes before and after row images
DELETEYesFull row image requires a primary key or REPLICA IDENTITY FULL
TRUNCATEYesPropagated as a delete-all event to the sink
DDL changesDetection onlySchema drift is detected and the pipeline pauses for review; DDL is not replayed automatically

Monitoring

nanosync metrics pipeline orders-pipeline

Monitor the replication slot directly in PostgreSQL:

SELECT
  slot_name,
  active,
  confirmed_flush_lsn,
  pg_size_pretty(
    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
  ) AS lag
FROM pg_replication_slots
WHERE slot_name = 'nanosync_slot_orders';

Key Prometheus metrics:

MetricDescription
ns_pipeline_replication_lag_secondsEnd-to-end source-to-sink latency
ns_cdc_events_totalCDC events received from the slot
ns_snapshot_rows_totalRows written during the initial snapshot

Limitations

Troubleshooting

ERROR: logical replication slot "..." is active for PID ... Another process is consuming the slot. Check with:

SELECT pid, slot_name, active FROM pg_replication_slots;

ERROR: replication slot "..." does not exist The slot was dropped externally. Stop the pipeline, drop its checkpoint in Nanosync (nanosync delete pipeline <name>), and restart to trigger a fresh snapshot.

WAL accumulating / disk pressure The slot is not being consumed. Check pg_replication_slots.wal_status. If it shows lost, the slot has been invalidated and must be recreated (full re-snapshot required). To prevent this, keep slot_lag_alert_mb at a level appropriate for your disk capacity.

Pipeline stalls after a Postgres primary promotion Without failover_slots: "true", the replication slot does not exist on the new primary. Enable failover slots (PG 17+) or configure auto-restart with a fresh snapshot after failover.