Developer Documentation

Data Mirror

A production-grade Change Data Capture (CDC) synchronization engine. Continuously monitor database changes and propagate them to your sync backend in real time.

1 Introduction

The Data Mirror is a production-grade, containerized Change Data Capture (CDC) synchronization engine. It continuously monitors changes in one or more PostgreSQL or MySQL databases and propagates those changes in real time to a search engine of your choice (Meilisearch, Elasticsearch, OpenSearch, or any custom backend).

The engine is distributed as a Docker image. You configure it entirely through environment variables and a single YAML schema file that you mount into the container. There is no application code to write - unless you wish to integrate a custom sync backend or perform advanced data transformations via Python scripts.

Key Capabilities

Two CDC Modes

Debezium + Kafka for scale, or Direct Stream (WAL/Binlog) for zero-infrastructure simplicity.

Declarative YAML Schema

Support for one-to-one, one-to-many, many-to-one, and many-to-many relationships.

Python Transform Scripts

Built-in field transformations and User-Controlled Python Scripts for advanced logic.

Smart Batch Processing

Deduplication and configurable accumulation windows for optimal performance.

Initial Snapshot Support

Resume semantics to bulk-load tables without downtime.

2 Architecture Overview

The engine is composed of the following logical layers:

Source
PostgreSQL
WAL Mode
MySQL
Binlog Mode
CDC Capture
CDC Capture Layer
WAL / Binlog / Debezium
Broker
Kafka
Redis
Local
Processor
Event Processor
Transform + Relationships
Sink
Meilisearch
Elasticsearch
OpenSearch

CDC Modes in Detail

CDC_MODE=wal or postgres_wal PostgreSQL WAL Mode

The engine creates a PostgreSQL replication slot and publication, then streams row-level changes directly from the Write-Ahead Log.

  • Tracks position using LSN checkpoint file (/data/lsn_checkpoint.txt)
  • Resumes from last LSN on restart, ensuring zero data loss
  • Uses internal in-memory broker by default
CDC_MODE=mysql_binlog MySQL Binlog Mode

The engine connects to MySQL as a replica and reads the binary log directly.

  • Requires binlog_format=ROW and binlog_row_image=FULL
  • Automatically handles MySQL 8.4+ compatibility
  • Tracks position using JSON checkpoint file (/data/mysql_binlog_checkpoint.json)
  • Supports comma-separated table inclusion/exclusion via environment variables
CDC_MODE=debezium Debezium Mode

An external Debezium Connect instance monitors PostgreSQL and publishes change events to Kafka. Debezium applies the ExtractNewRecordState Single Message Transform (SMT), which flattens events into simple before/after records.

  • The engine waits for the Debezium Connect REST API to become available before starting
  • It automatically registers a Debezium connector for each database referenced in the schema
  • On shutdown, the engine deregisters its Debezium connectors for clean teardown
  • The Kafka topic naming convention follows: {server_name}.{schema}.{table}

3 System Requirements

Docker and Docker Compose (Mandatory)
PostgreSQL (v12+) with wal_level=logical (Required based on usage)
MySQL (v8.0+) with binlog_format=ROW (Required based on usage)
A search engine (sink) - Meilisearch, Elasticsearch, or OpenSearch (Required based on usage)
Python (v3.9+) (Required based on usage)

Installation & Setup Guide

1. Docker & Docker Compose

Required to run the Data Mirror engine and its peripheral services.

2. Python v3.9+

Required for running custom transformation scripts or local testing utilities.

Ubuntu / Debian
sudo apt update
sudo apt install python3 python3-pip
macOS
# Requires Homebrew
brew install python

Windows users can download the installer from python.org.

3. Databases & Search Engines

Use Docker to quickly initialize your source and sink environments.

Run Meilisearch (Sink)
docker run -itd -p 7700:7700 getmeili/meilisearch:latest
Run PostgreSQL (Source)
docker run -d --name pg-source \
  -e POSTGRES_PASSWORD=pass \
  -p 5432:5432 \
  postgres:latest -c wal_level=logical
Verify PostgreSQL WAL Level
psql -c "SHOW wal_level;"
-- Should return: logical

# If it shows replica or minimal, update postgresql.conf:
# In postgresql.conf
wal_level = logical

# Then restart PostgreSQL
sudo systemctl restart postgresql

4 Quick Start

Step 1: Create Your Schema File

Create a file called schema.yaml that defines which tables to sync and how to map them.

schema.yaml
schemas:
  - database: "mydb"
    index: "products"
    enabled: true
    table: "products"
    schema: "public"
    primary_key: "id"
    columns:
      - id
      - name
      - price
      - description
    searchable_attributes:
      - name
      - description
    filterable_attributes:
      - price
    sortable_attributes:
      - price

Step 2: Run the Data Mirror Engine

Option A: WAL Mode (Simplest)

bash
docker run -d \
  --name data-mirror \
  -e SOURCE_DB_TYPE=postgres \
  -e CDC_MODE=wal \
  -e POSTGRES_HOST=host.docker.internal \
  -e POSTGRES_PORT=5432 \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=your_password \
  -e SINK_BACKEND=meilisearch \
  -e SINK_HOST=http://host.docker.internal:7700 \
  -e SINK_API_KEY=your_master_key \
  -v ./schema.yaml:/app/schema.yaml \
  --add-host=host.docker.internal:host-gateway \
  data-mirror
OR

Option B: MySQL Binlog Mode

bash
docker run -d \
  --name data-mirror \
  -e SOURCE_DB_TYPE=mysql \
  -e CDC_MODE=mysql_binlog \
  -e MYSQL_HOST=host.docker.internal \
  -e MYSQL_PORT=3306 \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=your_password \
  -e MYSQL_CDC_ENABLED=true \
  -e SINK_BACKEND=meilisearch \
  -e SINK_HOST=http://host.docker.internal:7700 \
  -e SINK_API_KEY=your_master_key \
  -v ./schema.yaml:/app/schema.yaml \
  --add-host=host.docker.internal:host-gateway \
  data-mirror
OR

Option C: Debezium + Kafka Mode

bash
docker run -d \
  --name data-mirror \
  -e SOURCE_DB_TYPE=postgres \
  -e CDC_MODE=debezium \
  -e BROKER_TYPE=kafka \
  -e KAFKA_BROKER=kafka:29092 \
  -e KAFKA_GROUP_ID=data-mirror \
  -e KAFKA_AUTO_OFFSET_RESET=earliest \
  -e DEBEZIUM_SERVER_NAME=dbserver1 \
  -e CONNECT_HOST=http://debezium:8083 \
  -e POSTGRES_HOST=host.docker.internal \
  -e POSTGRES_PORT=5432 \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=your_password \
  -e SINK_BACKEND=meilisearch \
  -e SINK_HOST=http://host.docker.internal:7700 \
  -e SINK_API_KEY=your_master_key \
  -v ./schema.yaml:/app/schema.yaml \
  --add-host=host.docker.internal:host-gateway \
  data-mirror

Alternatively, use Docker Compose for a complete Debezium + Kafka stack:

docker-compose.yml
services:
  # Zookeeper for Kafka
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    hostname: zookeeper
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    networks:
      - cdc-network
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "2181"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

  # Kafka Broker
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    hostname: kafka
    container_name: kafka
    depends_on:
      zookeeper:
        condition: service_healthy
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_LOG_RETENTION_HOURS: 168
    networks:
      - cdc-network
    healthcheck:
      test:
        ["CMD-SHELL", "kafka-topics --bootstrap-server localhost:29092 --list"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 512M

  debezium:
    image: debezium/connect:3.0.0.Final
    hostname: debezium
    container_name: debezium
    depends_on:
      kafka:
        condition: service_healthy
    ports:
      - "8083:8083"
    environment:
      BOOTSTRAP_SERVERS: kafka:29092
      GROUP_ID: debezium-group
      CONFIG_STORAGE_TOPIC: debezium_configs
      OFFSET_STORAGE_TOPIC: debezium_offsets
      STATUS_STORAGE_TOPIC: debezium_statuses
      KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      CONNECT_KEY_CONVERTER_SCHEMAS_ENABLE: "false"
      CONNECT_VALUE_CONVERTER_SCHEMAS_ENABLE: "false"

      CONNECT_PRODUCER_BATCH_SIZE: 32768
      CONNECT_PRODUCER_LINGER_MS: 10
      # Increase memory
      KAFKA_HEAP_OPTS: "-Xms512m -Xmx1024m"
    networks:
      - cdc-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8083/connectors"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 512M

  data-mirror:
    image: data-mirror-preview
    container_name: data-mirror
    depends_on:
      kafka:
        condition: service_healthy
      debezium:
        condition: service_healthy
    ports:
      - 8080:8080
      - 8090:8090
    env_file:
      - .env
    volumes:
      - ./schema.yaml:/app/schema.yaml
      - ./transforms:/app/transformers
    extra_hosts:
      - host.docker.internal:host-gateway
    networks:
      - cdc-network

networks:
  cdc-network:
    driver: bridge
    name: cdc-network

Step 3: Verify

bash
# Check logs
docker logs -f data-mirror

# Check pipeline health (REST API)
curl -H "Authorization: Bearer changeme" http://localhost:8090/api/pipeline

# Check metrics (Prometheus)
curl http://localhost:8080/metrics

5 Environment Variable Reference

Core Configuration

Variable Default Description
SOURCE_DB_TYPE postgres Database type: postgres, mysql
CDC_MODE debezium The capture mode. Options: debezium, wal (or postgres_wal), mysql_binlog (or binlog).
BROKER_TYPE kafka Message broker for event transport. Options: kafka, redis, local.
INTERNAL_BROKER_ENABLED false When true, uses an in-memory queue for direct stream modes, bypassing external brokers.
SINK_BACKEND meilisearch Targeted search engine. Options: meilisearch, elasticsearch, opensearch.
LOG_LEVEL INFO Log level (DEBUG, INFO, WARNING, ERROR). Emitted as structured JSON.
TRANSFORM_SCRIPTS_DIR /app/transformers Directory to look for custom Python transform scripts specified in schema.yaml.
CUSTOM_BACKENDS_PATH /app/custom_backends Base directory where the engine scans for custom sync backend plugin folders.

Database Configuration

Variable Default Description
POSTGRES_HOST localhost PostgreSQL host
POSTGRES_PORT 5432 PostgreSQL port
POSTGRES_USER postgres PostgreSQL user (requires replication/superuser for WAL mode).
POSTGRES_PASSWORD postgres PostgreSQL password
MYSQL_HOST localhost MySQL host. (Used if SOURCE_DB_TYPE=mysql).
MYSQL_PORT 3306 MySQL port.
MYSQL_USER root MySQL user (requires REPLICATION SLAVE for binlog mode).
MYSQL_PASSWORD MySQL password.
DATABASE_POOL_SIZE 10 Number of connections to maintain per database.

Important: The database name is not configured here. Each schema entry in your schema.yaml specifies its own database field. The engine creates a separate connection pool for each unique database.

Variable Default Description
SINK_HOST The URL of the search engine instance (e.g., http://localhost:7700 for Meilisearch, http://localhost:9200 for Elasticsearch). This is read directly by the sync backend plugin.
SINK_API_KEY API key or authentication token for the search engine. For Meilisearch, this is the master key. For Elasticsearch, this may be the API key or basic auth credentials.

Kafka Broker Configuration

Variable Default Description
KAFKA_BROKER localhost:9092 Kafka bootstrap servers.
KAFKA_GROUP_ID data-mirror Consumer group ID.
KAFKA_AUTO_OFFSET_RESET earliest Where to start consuming if no committed offset exists. Options: earliest(from the beginning) or latest(only new messages).

Redis Broker Configuration

Variable Default Description
REDIS_HOST localhost Redis host.
REDIS_PORT 6379 Redis port.
REDIS_PASSWORD Redis password.
REDIS_DB 0 Redis database number.

Debezium Configuration

Variable Default Description
CONNECT_HOST http://localhost:8083 Debezium Connect REST API URL.
DEBEZIUM_SERVER_NAME dbserver1 Prefix for Kafka topics.

Direct Stream Configuration (Postgres & MySQL)

Variable Default Description
PG_REPLICATION_SLOT sync_engine_slot PostgreSQL logical replication slot name.
PG_PUBLICATION_NAME sync_engine_pub PostgreSQL publication name.
MYSQL_CDC_ENABLED false Must be true for mysql_binlog mode.
MYSQL_BINLOG_CHECKPOINT_PATH /data/binlog_checkpoint.json File to persist MySQL binlog position.
MYSQL_BINLOG_SERVER_ID 5401 Unique ID for the MySQL binlog reader.

Sync & Batch Processing Configuration

Variable Default Description
SCHEMA_PATH /app/schema.yaml Path inside the container where the engine looks for the schema YAML file.
ENABLE_BATCHING true When enabled, events are accumulated into batches before processing. When disabled, each event is processed individually.
SYNC_MAX_BATCH_SIZE 500 Maximum number of CDC events to accumulate in a single batch before triggering processing.
SYNC_BATCH_DELAY_MS 2000 Maximum time in milliseconds to wait while accumulating events before flushing the batch.
SYNC_POLLING_STRATEGY linger Strategy for event polling. Options: linger (latency-optimized) or deadline (throughput-optimized).
SYNC_MAX_SINK_BATCH_SIZE 1000 Maximum number of documents to send to the sink backend in a single bulk API call.
SYNC_MAX_RELATION_FAN_OUT 1000 Maximum number of parent records to re-index when a related (child or junction) table changes.
SYNC_INITIAL_SNAPSHOT false When set to true, the engine performs a full table scan of all configured schemas on startup.
SYNC_SNAPSHOT_BATCH_SIZE 500 Number of rows to fetch per batch during the initial snapshot.
SYNC_LSN_CHECKPOINT_PATH /data/lsn_checkpoint.txt File path inside the container where the WAL LSN checkpoint is persisted.
HEALTH_CHECK_PORT 8080 Port for the HTTP health check server. Exposes /health, /ready, /metrics, and /stats endpoints.
ENABLE_PROMETHEUS true Enable or disable the Prometheus metrics server.
METRICS_PORT 9090 Port for the dedicated Prometheus metrics HTTP server.
ENABLE_METRICS true Master toggle for all metrics collection.
ENVIRONMENT production Application environment setting (e.g., production, development).
LOG_LEVEL INFO Logging verbosity. Options: DEBUG, INFO, WARNING, ERROR.
LOG_FORMAT json Format of the stdout logs. Options: json, console.
DATABASE_MAX_OVERFLOW 20 Maximum number of additional connections to open beyond the pool size during high concurrency.
SYNC_BATCH_QUERY_TIMEOUT_SECONDS 300 Maximum time allowed for large bulk enrichment queries before timing out.
API_ENABLED true Enable or disable the REST API for management and monitoring.
API_PORT 8090 Port for the REST API server.
DASHBOARD_API_KEY changeme Basic authentication token required to access the REST API endpoints.

6 Schema Configuration Reference

The schema file is the heart of the sync engine. It is a YAML file mounted at /app/schema.yaml. It contains a list of schema definitions under the top-level schemas key.

6.1 Root Schema Properties

schema.yaml
schemas:
  - database: "mydb"           # Source database name
    index: "products"          # Target search index name (created automatically)
    enabled: true              # Set to false to disable this sync task
    table: "products"          # Source table name
    schema: "public"           # Database schema/namespace (Postgres only)
    primary_key: "id"          # The primary key column for the target index
    columns: ["*"]             # List of columns to sync. Use ["*"] for all.

    # Advanced Properties
    ranking_rules: ["words", "typo", "proximity", "attribute", "sort", "exactness"] # Meilisearch optimization
    filters: ["category", "price"] # List of filterable attributes (sync backend dependent)
    where_clause: "price > 0 AND status = 'active'" # SQL filter for snapshot fetching
    transform_script: "product_cleaner.py" # Path to a custom Python transformation script

6.2 Transformations

Transformations are applied after data is fetched from the database and before it is sent to the sync backend.

transform
transform:
  # Rename columns in the output document
  rename:
    style_code: "styleId"
    business_name: "businessName"

  # Cast column values to specific types
  cast:
    price: "float"
    stock: "int"
    allow_backorder: "bool"

  # Compute derived fields
  computed:
    - field: "discount_percentage"
      expression: "((mrp - price) / mrp * 100) if mrp > 0 else 0"
    - field: "in_stock"
      expression: "stock > 0"

6.3 Relationships

One-to-One

A single related record is embedded as a JSON object.

relationships:
  - name: "vendor"
    type: "one_to_one"
    table: "vendors"
    join_condition:
      parent_key: "vendor_id"
      child_key: "id"

One-to-Many

Multiple related records are embedded as a JSON array.

relationships:
  - name: "tags"
    type: "one_to_many"
    table: "product_tags"
    join_condition:
      parent_key: "id"
      child_key: "product_id"

Many-to-Many

Joins through a junction table.

relationships:
  - name: "categories"
    type: "many_to_many"
    table: "categories"
    through_table: "product_categories"
    join_condition:
      parent_key: "id"
      through_parent_key: "product_id"
      through_child_key: "category_id"
      child_key: "id"

6.4 Python Transform Scripts

For complex logic that cannot be expressed via YAML (e.g. conditional field updates, external data enrichment, or complex calculations), you can provide a custom Python script.

How to use

  1. Create a .py file (e.g. transform_product.py).
  2. Mount the folder containing the script to /app/transformers.
  3. In schema.yaml, set transform_script: transform_product.py.
transform_product.py
def transform(doc: dict) -> dict:
    # Perform custom logic
    if doc.get("price") > 1000:
        doc["is_premium"] = True
    
    # Return the modified dict
    return doc

Workflow: The script is loaded lazily on first use. If an error occurs inside your transform() function, the engine logs the traceback but continues processing with the original document to prevent pipeline failure.

7 Initial Snapshot

When deploying against an existing database, set SYNC_INITIAL_SNAPSHOT=true to perform a full table scan on startup.

Phase A Snapshot

Records current WAL LSN, iterates through all rows using cursor-based pagination, and pushes to sync backend.

Phase B Streaming

After snapshot completes, WAL streaming resumes from recorded LSN, catching up on any changes during snapshot.

Tip: Mount a Docker volume at /data to persist the LSN checkpoint across container restarts. This prevents re-running the snapshot.

8 Management REST API (Port 8090)

All requests must include an Authorization: Bearer {DASHBOARD_API_KEY} header.

Configuration Endpoints

GET /api/config/schemas Returns active schema mappings
GET /api/config/settings Returns engine configuration and connection health
POST /api/config/sync/{index_name} Triggers manual re-sync for specified index
POST /api/config/reload Reloads schema and Python scripts from disk

Pipeline Monitoring

GET /api/pipeline Comprehensive health check with WAL lag, rates, memory
GET /api/pipeline/recent_events Returns 50 most recent CDC events

8.3 DLQ & Error Management

The engine includes a Dead-Letter Queue (DLQ) for records that fail processing. These endpoints allow you to inspect, retry, or purge failed records.

GET /api/errors Paginated, searchable list of failed records
GET /api/errors/stats Error rate, health scores, and DLQ statistics
GET /api/errors/{id} Get detailed error payload and traceback
POST /api/errors/{id}/retry Immediately retry a specific failed record
POST /api/errors/retry-all Drain broker DLQ and retry all messages
POST /api/errors/retry-all-inmemory Retry all retryable in-memory DLQ records
DELETE /api/errors Purge all records from the DLQ

9 Monitoring & Observability

Health Check Endpoints (Port 8080)

Endpoint Description
/health Liveness probe. Returns 200 if process is running.
/ready Readiness probe. Returns 200 when processor is initialized.
/metrics Prometheus metrics in text exposition format.
/stats Human-readable JSON summary of current metrics.

Prometheus Metrics

cdc_events_processed_total Counter
cdc_events_failed_total Counter
cdc_event_processing_duration_seconds Histogram
cdc_kafka_consumer_lag Gauge
cdc_db_queries_total Counter

10 Production Deployment

1

Persist the LSN Checkpoint

Always mount a volume at /data to resume from last position after restart.

2

Use WAL Mode for Simplicity

Unless you have Kafka infrastructure, WAL mode is simpler with fewer moving parts.

3

Tune Batch Parameters

Increase SYNC_MAX_BATCH_SIZE for throughput, decrease for latency requirements.

4

Set SYNC_MAX_RELATION_FAN_OUT

Prevent runaway re-indexing for broadly-referenced lookup tables.

5

Monitor with Prometheus

Scrape /metrics endpoint and set alerts on cdc_events_failed_total and consumer lag.

6

PostgreSQL Replication User

Ensure user has REPLICATION privilege: ALTER USER postgres WITH REPLICATION;

11 Advanced Operational Topics

11.1 Multi-Database Synchronization

The Data Mirror can synchronize data from multiple independent databases in a single instance. This is achieved by specifying different database names in your schema.yaml.

  • The engine maintains a separate connection pool for each unique database found in the schema.
  • Global connection settings (Host, Port, User, Password) apply to all databases.
  • For PostgreSQL, ensure the user has the REPLICATION attribute on all target databases.

11.2 Broker Decoupling & Kafka-less Operation

Historically, Debezium required Kafka for schema history storage. Data Mirror now allows for "Kafka-less" operation in several ways:

1. Direct Streams

Using CDC_MODE=wal or mysql_binlog with INTERNAL_BROKER_ENABLED=true removes the need for any external broker (Kafka or Redis) entirely.

2. Redis Broker

You can use BROKER_TYPE=redis to use Redis as the primary message transport instead of Kafka.

3. File-based Schema History

When running Debezium without Kafka, the engine automatically configures Debezium to use local file-based schema history (FileSchemaHistory), persisting it in the /data directory.

12 Smart Batch Processing

The engine employs an intelligent batching strategy to minimize database queries and sync backend API calls:

1

Event Accumulation

CDC events are buffered in memory for up to SYNC_BATCH_DELAY_MS milliseconds or until SYNC_MAX_BATCH_SIZE events are collected.

2

Deduplication

Within a batch, if the same record ID appears multiple times (e.g. rapid updates), only the last event is kept.

3

Relationship Resolution

When child/junction rows change, the engine identifies affected parent record IDs and fetches complete documents in bulk.

4

Bulk Fetch

Optimized recursive JSON queries fetch complete document trees in a single database round-trip per batch.

5

Bulk Push

Documents are sent using batch APIs, respecting the SYNC_MAX_SINK_BATCH_SIZE limit.

13 Pre-flight Checks

Before processing any events, the engine runs a series of pre-flight checks to validate connectivity and configuration:

  • 1. Debezium Connectivity (Debezium mode only): Verifies that the Connect REST API is reachable.
  • 2. Sync Backend Connectivity Connects to backend and performs a health check.
  • 3. Broker Connectivity Verifies configured message broker is reachable.
  • 4. Database Connectivity Verifies PostgreSQL/MySQL is reachable for each schema database.

If any pre-flight check fails, the engine logs a critical error and exits immediately, preventing silent failures.

14 Custom Sync Backend Plugin

You can integrate any search engine by creating a custom backend plugin. The plugin is loaded at runtime—no need to rebuild the Docker image.

Plugin Structure

manifest.json

Declares plugin metadata, dependencies, and class mapping.

backend.py

Contains the Python class implementing required interface.

manifest.json
{
  "name": "my_custom_search",
  "description": "My custom sync backend plugin",
  "requires": ["some_library"],
  "pip_dependencies": ["some_library>=1.0.0"],
  "backend_classes": {
    "my_custom": "MyCustomBackend"
  }
}

Required Interface

Your backend class must inherit from SinkBackendPlugin and implement the following asynchronous methods. For a full example, refer to the source of the ElasticsearchBackend distributed with the engine.

backend.py
from backends.base import SinkBackendPlugin

class MyCustomBackend(SinkBackendPlugin):
    async def connect(self):
        """Establish connection to the sink."""

    async def disconnect(self):
        """Close connection to the sink."""

    async def upsert_document(self, index_name, document, doc_id):
        """Insert or update a single document."""

    async def delete_document(self, index_name, doc_id):
        """Delete a document."""

    async def batch_upsert(self, index_name, documents, primary_key="id"):
        """Batch insert or update (list of (doc, id) tuples)."""

    async def batch_delete(self, index_name, doc_ids):
        """Batch delete multiple document IDs."""

    async def create_index(self, index_name, settings, primary_key="id"):
        """Create index with settings (ranking, filters, etc)."""

    async def delete_index(self, index_name):
        """Delete an index."""

    async def index_exists(self, index_name) -> bool:
        """Return True if index exists."""

    async def get_index_stats(self, index_name) -> dict:
        """Return index info (document count, size, etc)."""

    async def get_document_sample(self, index_name, limit=1) -> list:
        """Fetch a sample of documents for debugging/validation."""

    async def wait_for_pending_tasks(self, index_name):
        """Wait for background indexing tasks to settle."""

    async def health_check(self) -> bool:
        """Return True if the backend is reachable and healthy."""

Registration & Usage

1. Specify your backend via environment variable:

SINK_BACKEND=my_custom

2. Mount the plugin folder via Docker Compose:

volumes:
  - ./my_plugin:/app/custom_backends/my_custom

Or via docker run:

docker run -v ./my_plugin:/app/custom_backends/my_custom \
  -e SINK_BACKEND=my_custom \
  data-mirror-preview

15 Docker Volume Mounts

Volume Mounts

Host Path Container Path Purpose
./schema.yaml /app/schema.yaml Required. Your schema definition file.
volume or dir /data Recommended. Persists LSN checkpoint.
./my_backend/ /app/custom_backends/ Optional. Custom backend plugin.

16 Exposed Ports

8080 HTTP

Health check server

/health, /ready, /metrics, /stats

8090 HTTP

Management REST API

Configuration, pipeline control & DLQ management

17 Logging

The engine uses structured JSON logging by default, which is ideal for log aggregation systems like Elasticsearch or Datadog.

You can configure the log format and level using LOG_FORMAT and LOG_LEVEL environment variables.

18 Troubleshooting

Engine exits with "SYNC BACKEND NOT FOUND"
Verify SINK_BACKEND matches a registered backend alias. For custom backends, ensure the plugin directory is mounted correctly and contains both a valid manifest and backend module.
Schema file not found error
Ensure your schema YAML is mounted at the path specified by SCHEMA_PATH (default: /app/schema.yaml).
Events not appearing in search index
Check docker logs for errors. Verify the schema has enabled: true. Confirm the table and column names match the database exactly.
WAL replication slot not created
Ensure wal_level=logical is set in PostgreSQL and the user has REPLICATION privilege.
High consumer lag (Kafka mode)
Increase SYNC_MAX_BATCH_SIZE and SYNC_BATCH_DELAY_MS to process more events per cycle. Consider scaling Kafka partitions.
Custom backend pip install fails
Check pip_dependencies in your manifest. Ensure package names and version specifiers are valid. Container includes gcc and libpq-dev for C extensions.