README
¶
Sync Client V2
The Sync Client (dbsync2) can be used to transfer all updates on contacts to an SQL database or a web service.
Contents
- Activation — get the campaign token
- Quickstart — running in five minutes with the prebuilt binary
- Installation — binary, Docker, or build from source
- Troubleshooting — error messages and what they mean
- Usage — modes, tables, connection URLs, webhooks
- Complete Examples per Database — database setup and commands for PostgreSQL, MySQL/MariaDB, SQL Server
- Command Line Options — every option, grouped by purpose
- Caveats and Operational Notes — schema behaviour, campaigns with many fields, indexes, data types, privileges
Activation
To be able to use the Sync Client, activate the corresponding connector in the campaign settings.
After successful activation, a token is displayed below the connectors. It is required to use the client.
The token is only visible to the user who activated the connector.

Quickstart: PostgreSQL with the prebuilt binary
You do not need Go, Docker, or the source code to run dbsync2. Downloading the binary is enough.
1. Download the binary
Open the downloads page and pick the file with the highest version number for your platform:
| Platform | File name |
|---|---|
| Linux (x86_64) | dbsync2_<VERSION> |
| Windows (x86_64) | dbsync2_<VERSION>.exe |
| macOS (Intel) | dbsync2_<VERSION>_mac |
| macOS (Apple Silicon) | dbsync2_<VERSION>_mac_arm64 |
# Linux — replace 1.20.2 with the highest version on the downloads page
curl -L -o dbsync2 https://bitbucket.org/modima/dbsync2/downloads/dbsync2_1.20.2
chmod +x dbsync2
./dbsync2 --version
2. Create the target database
dbsync2 creates its tables, but it does not create the database — that must already exist. Installing PostgreSQL is not sufficient.
sudo -u postgres psql <<'SQL'
CREATE USER dbsync WITH PASSWORD 'my_password';
CREATE DATABASE my_database OWNER dbsync;
SQL
Note the database name, user, and password — all three go into the connection URL.
3. Import the existing data (once)
db_init fetches the complete campaign history and then exits. Depending on the campaign size this can take a while.
./dbsync2 --a db_init \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'postgres://dbsync:my_password@localhost:5432/my_database?sslmode=disable'
4. Keep the database up to date (continuously)
db_sync runs until it is stopped and picks up where the previous run left off.
./dbsync2 --a db_sync \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'postgres://dbsync:my_password@localhost:5432/my_database?sslmode=disable'
Add --v to see the log on stdout instead of writing it to a log file.
Things that are easy to get wrong
--a(mode) is required. Without it the client aborts; there is no default mode.- Everything is a flag. There are no positional arguments — the connection URL must be passed as
--url '...', not appended to the command line. - Quote the URL. A URL containing
?,&, or special characters in the password must be single-quoted in the shell. - Use the right port. PostgreSQL
5432, MySQL/MariaDB3306, SQL Server1433. - The database must exist before the first run (see step 2).
--s(start date) is optional. Omit it anddb_initfetches everything,db_syncresumes from its saved state, anddb_updatestarts one week ago.--fm/--fp(filters) are optional. Omit them to sync everything.
Installation
Download the latest version of the binary
- Linux/Windows/Mac — see the Quickstart above. This is the recommended way to install dbsync2.
Run via Docker
docker run --rm dialfire/dbsync2:latest dbsync2 --version
See Docker Compose Deployment below for a full setup.
Build from source
dbsync2 is a Go module, so no GOPATH setup and no godep are needed. Clone the repository and build it in place:
sudo apt-get install git golang-go # Go 1.23 or newer is required
git clone https://bitbucket.org/modima/dbsync2.git
cd dbsync2
go build -o dbsync2
./dbsync2 --version
Cross-compiling for other platforms from the same checkout:
# Linux (static, as used for the released binary)
CGO_ENABLED=0 GOOS=linux go build -o dbsync2
# Windows
GOOS=windows GOARCH=amd64 go build -o dbsync2.exe
# macOS (Intel / Apple Silicon)
GOOS=darwin GOARCH=amd64 go build -o dbsync2_mac
GOOS=darwin GOARCH=arm64 go build -o dbsync2_mac_arm64
go getandgo installdo not work for this repository.go get bitbucket.org/modima/dbsync2fails with "go.mod file not found in current directory or any parent directory" — since Go 1.17go getonly works inside an existing module and no longer installs commands.go install bitbucket.org/modima/dbsync2@latestdoes not resolve either, because the repository is not published withv-prefixed semver tags via a module proxy. Usegit clone+go buildas shown above, or simply download the prebuilt binary.
Troubleshooting
| Message | Cause | Fix |
|---|---|---|
Execution mode (--a) is required. Valid modes: ... |
--a was omitted |
Add --a db_init, --a db_sync, --a db_update, or --a webhook |
Invalid mode 'xyz' |
Typo in the mode name | Use one of webhook, db_init, db_update, db_sync |
Error parsing command line: unknown flag: --ss |
Typo in a flag name — parsing stops there, so all following flags (e.g. --url) are ignored |
Correct the flag name (--s is the start date); run dbsync2 --help for the full list |
Unexpected argument 'postgres://...' |
The URL was passed positionally | Pass it as --url 'postgres://...' |
database connection URL (--url) is required in mode 'db_sync' |
--url missing, or dropped because of a typo in an earlier flag |
Add --url '...' and check the other flag names |
unsupported database driver 'oracle' |
Unsupported or misspelled URL scheme | Use postgres://, mysql://, or sqlserver:// |
Database name is missing from --url ... |
URL ends after the host/port | Append /my_database |
dial tcp 127.0.0.1:1433: connect: connection refused |
Wrong port or server not running | PostgreSQL 5432, MySQL 3306, SQL Server 1433 |
FATAL: database "my_database" does not exist |
The database was never created | See step 2 of the Quickstart |
403 Forbidden from the API |
Wrong campaign ID or token, or the connector is not activated | Re-copy the token from the campaign settings (Activation) |
pq: prepared statement does not exist (PgBouncer) |
Version older than v1.18 | Upgrade, or set PgBouncer to pool_mode=session — see PgBouncer Compatibility |
Error 1118: Row size too large ... is 8126 (MySQL) |
The campaign has too many fields for one InnoDB row — see Campaigns with many fields | Restrict the columns with --cf / --cfx, then drop the columns you do not need |
Error 1193: Unknown system variable 'useSSL' (MySQL) |
JDBC parameter in the URL; the Go driver forwards unknown parameters to the server | Use ?tls=false instead of ?useSSL=false |
Note that postgresql:// is accepted as an alias for postgres:// (likewise mariadb:// for mysql:// and mssql:// for sqlserver://) as of v1.20.2. Earlier versions only accept the canonical scheme names.
Local Release Flow
For local releases (no CI), use the helper scripts:
./preflight.sh
./release.sh
preflight.sh checks that Docker is running, the working tree is clean, and the version tag does not already exist.
release.sh runs the build, tags the repo, pushes the Docker image, and appends the digest to RELEASES.md.
To verify a released image:
./verify-image.sh
To run preflight for an existing tag (e.g., current release), set:
ALLOW_EXISTING_TAG=1 ./preflight.sh
Docker Compose Deployment
For new users and those who prefer containerized deployment, we provide a pre-configured docker-compose.yml that supports all modes (PostgreSQL, MySQL/MariaDB, SQL Server, and Webhook). With Docker Compose you can run dbsync2 without building or modifying any source code.
Prerequisites
- Docker installed
- Docker Compose installed
Configuration
Create a .env file in the same directory as your docker-compose.yml with the following content (adjust the values as needed):
# Campaign settings
CAMPAIGN_ID=my_campaign_id
CAMPAIGN_TOKEN=my_campaign_token
# PostgreSQL settings
POSTGRES_DB=mydb
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
# MySQL settings
MYSQL_DB=mydb
MYSQL_USER=myuser
MYSQL_PASSWORD=mypassword
MYSQL_ROOT_PASSWORD=myrootpassword
# SQL Server settings
SA_PASSWORD=YourStrong!Passw0rd
SQLSERVER_DB=mydb
# Webhook settings
WEBHOOK_URL=https://example.com/api/transactions/
docker-compose.yml
Below is a sample docker-compose.yml file that uses your Docker Hub image (dialfire/dbsync2:latest) and dynamically reads all connection details from the .env file:
version: "3.8"
services:
#############################
# PostgreSQL Example
#############################
postgres:
image: postgres:16
container_name: postgres_sync
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-mydb}
POSTGRES_USER: ${POSTGRES_USER:-myuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mypassword}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
dbsync2_postgres:
image: dialfire/dbsync2:latest
container_name: dbsync2_postgres
restart: unless-stopped
depends_on:
- postgres
environment:
CAMPAIGN_ID: ${CAMPAIGN_ID}
CAMPAIGN_TOKEN: ${CAMPAIGN_TOKEN}
POSTGRES_USER: ${POSTGRES_USER:-myuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mypassword}
POSTGRES_DB: ${POSTGRES_DB:-mydb}
command: >
dbsync2 --a db_sync --c ${CAMPAIGN_ID} --ct ${CAMPAIGN_TOKEN} --url postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
#############################
# MySQL/MariaDB Example
#############################
mysql:
image: mysql:8
container_name: mysql_sync
restart: unless-stopped
environment:
MYSQL_DATABASE: ${MYSQL_DB:-mydb}
MYSQL_USER: ${MYSQL_USER:-myuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-mypassword}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
volumes:
- ./data/mysql:/var/lib/mysql
ports:
- "3306:3306"
dbsync2_mysql:
image: dialfire/dbsync2:latest
container_name: dbsync2_mysql
restart: unless-stopped
depends_on:
- mysql
environment:
CAMPAIGN_ID: ${CAMPAIGN_ID}
CAMPAIGN_TOKEN: ${CAMPAIGN_TOKEN}
MYSQL_USER: ${MYSQL_USER:-myuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-mypassword}
MYSQL_DB: ${MYSQL_DB:-mydb}
command: >
dbsync2 --a db_sync --c ${CAMPAIGN_ID} --ct ${CAMPAIGN_TOKEN} --url mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DB}?tls=false
#############################
# SQL Server Example
#############################
sqlserver:
image: mcr.microsoft.com/mssql/server:2019-latest
container_name: sqlserver_sync
restart: unless-stopped
environment:
SA_PASSWORD: ${SA_PASSWORD:-YourStrong!Passw0rd}
ACCEPT_EULA: "Y"
MSSQL_PID: "Developer"
ports:
- "1433:1433"
dbsync2_sqlserver:
image: dialfire/dbsync2:latest
container_name: dbsync2_sqlserver
restart: unless-stopped
depends_on:
- sqlserver
environment:
CAMPAIGN_ID: ${CAMPAIGN_ID}
CAMPAIGN_TOKEN: ${CAMPAIGN_TOKEN}
SA_PASSWORD: ${SA_PASSWORD:-YourStrong!Passw0rd}
SQLSERVER_DB: ${SQLSERVER_DB:-mydb}
command: >
dbsync2 --a db_sync --c ${CAMPAIGN_ID} --ct ${CAMPAIGN_TOKEN} --url sqlserver://sa:${SA_PASSWORD}@sqlserver:1433/${SQLSERVER_DB}
#############################
# Webhook Example
#############################
dbsync2_webhook:
image: dialfire/dbsync2:latest
container_name: dbsync2_webhook
restart: unless-stopped
environment:
CAMPAIGN_ID: ${CAMPAIGN_ID}
CAMPAIGN_TOKEN: ${CAMPAIGN_TOKEN}
WEBHOOK_URL: ${WEBHOOK_URL:-https://example.com/api/transactions/}
command: >
dbsync2 --a webhook --c ${CAMPAIGN_ID} --ct ${CAMPAIGN_TOKEN} --url ${WEBHOOK_URL}
networks:
default: {}
Running via Docker Compose
Once your .env file is configured and the docker-compose.yml file is in place, simply run:
docker-compose up -d
Docker Compose will pull the pre-built dialfire/dbsync2:latest image and start the services with your specified configuration.
How It Works
All updates on contacts are loaded every minute and then transferred directly to the web service or database.
Usage
Required Options
Every invocation needs at least these four flags:
| Option | Description |
|---|---|
--a |
Execution mode: webhook, db_init, db_update or db_sync (see below) |
--c |
Campaign ID |
--ct |
Campaign token (see Activation) |
--url |
Target: database connection URL, or webhook URL in mode webhook |
All other options are optional. All flags can also be set via environment variables prefixed with DBSYNC_ in upper case (e.g. DBSYNC_CT=... for --ct); an explicitly passed flag always wins over the environment variable. This requires v1.21.0 or newer — in earlier versions the DBSYNC_* variables were silently ignored.
Execution Modes (--a)
| Mode | Behaviour | When to use it |
|---|---|---|
db_init |
Writes the complete campaign history to the database, then exits. | Once, for the initial setup. |
db_sync |
Writes all new updates continuously and does not exit. Resumes from the last processed timestamp. | Normal production operation, after db_init. |
db_update |
Writes all updates since --s (default: one week ago), then exits. |
One-off catch-up after downtime or to backfill a date range. |
webhook |
Sends every transaction and inbound call to a web service and does not exit. | Integration with an external service instead of a database. |
The typical sequence for a new database is db_init once, then db_sync permanently.
db_init and db_update do the same work and differ only in their default start date: db_init without --s fetches everything, db_update without --s starts one week ago. Passing --s to db_init limits it to that date just like db_update does.
SQL Database
The client currently supports the following database systems:
- MySQL / MariaDB
- PostgreSQL
- Microsoft SQL Server
The database itself must be created before the first run — dbsync2 creates only the tables inside an existing database, it does not issue CREATE DATABASE. See step 2 of the Quickstart.
The client creates the following tables within that database:
- contacts - Contains all $ fields, as well as the custom fields of the campaign.
- transactions - Contains all transactions and the reference contact_id to the corresponding contact.
- connections - Contains all connections of the transaction and the reference transaction_id to the corresponding transaction.
- recordings - Contains all call recordings of the connection and the reference connection_id to the corresponding connection.
- inbound_calls - Contains all inbound calls and the reference contact_id to the corresponding contact.
The *_id columns are references by convention — no FOREIGN KEY constraints and no indexes on them are created. See Caveats and Operational Notes.

contacts
| Column | Type | Description |
|---|---|---|
| $id | varchar(50) | Primary key - unique contact identifier |
| $md5 | varchar(50) | MD5 hash of contact data (used for change detection) |
| $ref | varchar(50) | External reference ID |
| $version | varchar(50) | Contact version |
| $campaign_id | varchar(50) | Campaign identifier |
| $task_id | varchar(50) | Current task identifier |
| $task | varchar(50) | Current task name |
| $status | varchar(50) | Contact status |
| $status_detail | varchar(100) | Detailed status information |
| $created_date | varchar(50) | Creation date |
| $entry_date | varchar(50) | Entry date into the campaign |
| $owner | varchar(50) | Owner/agent assigned |
| $follow_up_date | varchar(50) | Scheduled follow-up date |
| $phone | varchar(50) | Phone number |
| $timezone | varchar(50) | Contact timezone |
| $caller_id | varchar(50) | Caller ID used for outbound calls |
| $source | varchar(50) | Contact source |
| $comment | text | Comments/notes |
| $error | varchar(50) | Last error message |
| $recording | varchar(100) | Recording filename |
| $recording_url | varchar(100) | Recording URL |
| $changed | varchar(50) | Last modification timestamp |
| (custom fields) | text / numeric | One column per campaign field, added automatically as fields are added to the campaign |
transactions
| Column | Type | Description |
|---|---|---|
| id | varchar(50) | Primary key - unique transaction identifier |
| contact_id | varchar(50) | Foreign key → contacts.$id |
| task_id | varchar(50) | Task identifier |
| task | varchar(50) | Task name |
| status | varchar(50) | Transaction status |
| status_detail | varchar(100) | Detailed status information |
| fired | varchar(50) | Timestamp when transaction was fired |
| pause_time_sec | numeric | Pause time in seconds |
| edit_time_sec | numeric | Edit time in seconds |
| wrapup_time_sec | numeric | Wrap-up time in seconds |
| wait_time_sec | numeric | Wait time in seconds |
| user | varchar(50) | User/agent ID |
| user_loginName | varchar(50) | User login name |
| user_branch | varchar(50) | User branch |
| user_tenantAlias | varchar(50) | User tenant alias |
| actor | varchar(50) | Actor type (user, system, etc.) |
| type | varchar(50) | Transaction type |
| result | varchar(50) | Transaction result |
| trigger | varchar(50) | What triggered the transaction |
| isHI | boolean | Human interaction flag |
| revoked | boolean | Whether transaction was revoked |
| $changed | varchar(50) | Last modification timestamp |
connections
| Column | Type | Description |
|---|---|---|
| id | varchar(50) | Primary key - unique connection identifier |
| parent_connection_id | varchar(50) | Foreign key → connections.id (for transfers) |
| transfer_target_address | varchar(50) | Transfer target address |
| call_uuid | varchar(50) | Call UUID |
| global_call_uuid | varchar(100) | Global call UUID |
| transaction_id | varchar(50) | Foreign key → transactions.id |
| task_id | varchar(50) | Task identifier |
| contact_id | varchar(50) | Foreign key → contacts.$id |
| phone | varchar(50) | Phone number dialed |
| user | varchar(50) | User/agent ID |
| actor | varchar(50) | Actor type |
| hangup_party | varchar(50) | Who hung up (customer, agent, system) |
| isThirdPartyConnection | varchar(50) | Third-party connection flag |
| dialerdomain | varchar(50) | Dialer domain |
| fired | varchar(50) | Timestamp when connection was initiated |
| started | varchar(50) | Call start time |
| initiated | varchar(50) | Call initiation time |
| connected | varchar(50) | Call connection time |
| disconnected | varchar(50) | Call disconnection time |
| ended | varchar(50) | Call end time |
| duration | numeric | Call duration in seconds |
| remote_number | varchar(50) | Remote party phone number |
| line_number | varchar(50) | Line phone number |
| technology | varchar(50) | Connection technology |
| result | varchar(50) | Connection result |
| code | numeric | Result code |
| call_initiated | varchar(50) | Call initiated timestamp |
| call_connected | varchar(50) | Call connected timestamp |
| call_disconnected | varchar(50) | Call disconnected timestamp |
| $changed | varchar(50) | Last modification timestamp |
recordings
| Column | Type | Description |
|---|---|---|
| id | varchar(50) | Primary key - unique recording identifier |
| contact_id | varchar(50) | Foreign key → contacts.$id |
| connection_id | varchar(50) | Foreign key → connections.id |
| started | varchar(50) | Recording start time |
| stopped | varchar(50) | Recording stop time |
| filename | varchar(100) | Recording filename |
| location | varchar(100) | Recording storage location/URL |
| $changed | varchar(50) | Last modification timestamp |
inbound_calls
| Column | Type | Description |
|---|---|---|
| id | varchar(50) | Primary key - unique inbound call identifier (call_id) |
| line_id | varchar(50) | Line identifier |
| campaign_id | varchar(50) | Campaign identifier |
| task_id | varchar(50) | Task identifier |
| task_name | varchar(50) | Task name |
| contact_id | varchar(50) | Foreign key → contacts.$id (if associated) |
| remote_number | varchar(50) | Caller's phone number |
| line_number | varchar(50) | Called phone number |
| started | varchar(50) | Call start time |
| connected | varchar(50) | Agent connection time |
| disconnected | varchar(50) | Call end time |
| user | varchar(50) | Agent ID who handled the call |
| state | varchar(50) | Call state (open, talking, done, handled_by_ivr, lost) |
| connectable_time | varchar(50) | When call became connectable |
| disposition | varchar(50) | Call termination type (normal_clearing, ooo, handled_by_ivr, transferred, rejected, error) |
| dispatch_error | boolean | True if connect_time set but was_connected is false |
| initial_line_id | varchar(50) | First line ID before IVR routing |
| initial_ivr_id | varchar(50) | First IVR script ID |
| ivr_id | varchar(50) | Last/effective IVR script ID |
| type | varchar(50) | Classification type (default, inbound, rebound) |
| hangup_party | varchar(50) | Who hung up (customer, agent, system) |
| $changed | varchar(50) | Last modification timestamp |
Database Connection URL Schema
The connection URL is passed with --url and should be single-quoted in the shell. Mind the default port of your DBMS — using the wrong one results in connection refused.
MySQL / MariaDB (default port 3306):
mysql://username:password@localhost:3306/database?tls=false
PostgreSQL (default port 5432):
postgres://username:password@localhost:5432/database?sslmode=disable
Microsoft SQL Server (default port 1433):
sqlserver://username:password@localhost:1433/database
sqlserver://username:password@localhost:1433/instance/database # named instance
Since v1.20.2 the aliases postgresql://, mariadb:// and mssql:// are accepted as well. Earlier versions only accept the canonical scheme names above and abort with Invalid database driver 'postgresql'.
To place the tables into a specific PostgreSQL schema, pass the search path as a connection option. The schema must already exist (CREATE SCHEMA my_schema;) and %3D is the URL-encoded =:
--url 'postgres://my_user:my_password@localhost:5432/my_database?sslmode=disable&options=-csearch_path%3Dmy_schema'
Web Service
As an alternative to a database, the transactions and inbound calls can be forwarded to a web service. The service must accept POST requests and reply with a status code between 200 and 299 upon success. Otherwise, the data will be resent (up to 10 attempts).
Transaction Payload
Transactions are sent with the following JSON format:
{
"contact": {...},
"transaction": {...},
"state": "new" | "updated"
}
- contact - Contains the contact details.
- transaction - Contains the corresponding transaction.
- state -
newfor a new transaction,updatedwhen the transaction is updated (e.g., when connection data is added later).
Inbound Call Payload
Inbound calls are sent with the following JSON format:
With contact (when the inbound call has a contact_id):
{
"contact": {...},
"inbound_call": {...},
"state": "new" | "updated"
}
Without contact (when the inbound call has no contact_id):
{
"inbound_call": {...},
"state": "new" | "updated"
}
- contact - Contains the contact details (only if the inbound call is associated with a contact).
- inbound_call - Contains the inbound call details (call_time, state, calling_number, called_number, disposition, etc.).
- state -
newfor a new inbound call,updatedwhen the call state changes (e.g., from "ringing" to "answered").
Rate Limiting
Webhook mode supports configurable rate limiting to protect your web service:
--wr- Requests per second (default: 5)--wb- Burst limit (default: 2 × wr)
Example
Transfer all future transactions and inbound calls in the campaign MY_CAMPAIGN to a Webservice with a rate limit of 10 requests per second:
./dbsync2 --a webhook --c MY_CAMPAIGN_ID --ct MY_CAMPAIGN_TOKEN --url 'https://example.com/api/transactions/' --wr 10 --wb 20
PgBouncer Compatibility
dbsync2 v1.18+ is fully compatible with PgBouncer transaction pooling mode (pool_mode=transaction) without any special configuration.
Technical Implementation:
- Uses pgx/v5 driver instead of lib/pq
- Configured with
DefaultQueryExecMode = QueryExecModeSimpleProtocolto avoid server-side prepared statements - Automatically applied to all PostgreSQL connections - no user configuration required
Version History:
- v1.18+: Uses pgx/v5 driver with simple protocol mode - fully compatible with PgBouncer transaction pooling
- v1.17: Attempted fix using direct SQL execution, but lib/pq driver still created server-side prepared statements internally
- v1.16 and earlier: Used explicit prepared statements, incompatible with transaction pooling
If you're using an older version and encounter errors like pq: prepared statement does not exist or bind message supplies ... parameters, please upgrade to v1.18+ or configure PgBouncer to use pool_mode=session for your database.
Sync Timing Options (db_sync)
In db_sync mode the client combines a per-minute live poll with a periodic catch-up that re-lists recently changed contacts. Connection data (e.g. the call duration of a connect) is sometimes attached to a contact with a delay; if it is attached after the contact has dropped out of the rolling catch-up window, it is only picked up once another event touches the contact. The following options make the relevant windows configurable. The defaults reproduce the previous behaviour, so omitting them changes nothing.
| Option | Short | Default | Description |
|---|---|---|---|
--catchup_window |
--cw |
24h |
How far back the periodic catch-up re-lists changed contacts. |
--catchup_interval |
--ci |
12h |
How often the periodic catch-up runs. |
--live_lookback |
--ll |
2m |
Look-back window of the per-minute live poll. Increase (e.g. 15m) so connection data attached a few minutes after the call is still picked up live. |
--live_interval |
--li |
1m |
How often the live poll runs. |
All values use Go duration syntax (e.g. 90m, 24h, 168h).
Example
Run a continuous sync where delayed connect connection data is reconciled for up to seven days:
dbsync2 --a db_sync --c MY_CAMPAIGN_ID --ct MY_CAMPAIGN_TOKEN --catchup_window 168h --url 'postgres://my_user:my_password@localhost:5432/my_database?sslmode=disable'
A wider catch-up window re-lists and re-fetches more contacts per cycle, which increases load. Tune
--catchup_window/--catchup_intervalto balance completeness against load. The live event-dedup cache automatically follows--live_lookback.
Log and State Files
- Startup and configuration errors are written directly to the console (stderr).
- All log messages are written to
/var/log/dbsync2/{MY_CAMPAIGN_ID}_{TIMESTAMP}.log. - If
/var/log/dbsync2/is not writable, the log falls back to$HOME/.dbsync2/log/{TIMESTAMP}.log. - Use
--vto print the log to stdout instead of writing it to a file, and--l 5for debug level output. This is the fastest way to see what a run is actually doing. - The resume timestamp is stored per campaign in
/var/opt/dbsync2/{MY_CAMPAIGN_ID}.json, falling back to$HOME/.dbsync2/{MY_CAMPAIGN_ID}.json. Delete this file to makedb_syncstart over from the--sstart date. - Connection URLs are written to the log with the password replaced by
***(as of v1.20.2). Older versions log the full URL including the password — take care when sharing log output.
Complete Examples per Database
Each example is self-contained: create the database and the user, import the history once with db_init, then keep it up to date with db_sync. Replace MY_CAMPAIGN_ID / MY_CAMPAIGN_TOKEN with the values from your campaign, and pick a real password.
The user dbsync2 connects with needs permission to create and alter tables in the target database, plus the usual SELECT / INSERT / UPDATE. It reads information_schema (respectively sys.tables) to detect existing columns.
PostgreSQL
1. Create database and user
sudo -u postgres psql <<'SQL'
CREATE USER dbsync WITH PASSWORD 'my_password';
CREATE DATABASE dialfire OWNER dbsync;
SQL
Making dbsync the owner is the simplest working setup. If you must use a database you do not own, grant the rights on the schema explicitly (required from PostgreSQL 15 on, where public is no longer writable by everyone):
GRANT CREATE, USAGE ON SCHEMA public TO dbsync;
2. Import the history once
./dbsync2 --a db_init \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'postgres://dbsync:my_password@localhost:5432/dialfire?sslmode=disable'
3. Sync continuously
./dbsync2 --a db_sync \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'postgres://dbsync:my_password@localhost:5432/dialfire?sslmode=disable'
Notes:
- Use
sslmode=require(orverify-full) instead ofdisablefor a remote server. - Column names contain
$and mixed case, so they must be double-quoted in queries:SELECT "$id", "$phone" FROM contacts; - Works behind PgBouncer in
pool_mode=transaction— see PgBouncer Compatibility. - To put the tables into a dedicated schema, add
&options=-csearch_path%3Dmy_schemaand create the schema beforehand.
MySQL / MariaDB
1. Create database and user
sudo mysql <<'SQL'
CREATE DATABASE dialfire CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'dbsync'@'%' IDENTIFIED BY 'my_password';
GRANT SELECT, INSERT, UPDATE, CREATE, ALTER, INDEX ON dialfire.* TO 'dbsync'@'%';
FLUSH PRIVILEGES;
SQL
2. Import the history once
./dbsync2 --a db_init \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'mysql://dbsync:my_password@localhost:3306/dialfire?tls=false'
3. Sync continuously
./dbsync2 --a db_sync \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'mysql://dbsync:my_password@localhost:3306/dialfire?tls=false'
Notes:
-
dbsync2 appends
charset=utf8mb4andcollation=utf8mb4_general_cito the connection automatically if you do not set them, so emojis and non-Latin scripts survive. Create the database with autf8mb4charset as shown, otherwise the tables inherit a narrower server default. -
Connection parameters are Go driver parameters, not JDBC ones. TLS is controlled by
tls=false/tls=true/tls=skip-verify— the JDBC spellinguseSSL=falseis not understood and is forwarded to the server asSET useSSL = false, which fails withError 1193: Unknown system variable 'useSSL'. Other useful parameters:timeout,readTimeout,writeTimeout,maxAllowedPacket. The full list is in the go-sql-driver documentation. -
mariadb://is accepted as an alias formysql://(v1.20.2+). -
Column names contain
$, so they must be quoted with backticks in queries:SELECT `$id`, `$phone` FROM contacts;
Microsoft SQL Server
1. Create database and login
CREATE DATABASE dialfire;
GO
CREATE LOGIN dbsync WITH PASSWORD = 'My_Strong_Password1';
GO
USE dialfire;
CREATE USER dbsync FOR LOGIN dbsync;
ALTER ROLE db_owner ADD MEMBER dbsync;
GO
2. Import the history once
./dbsync2 --a db_init \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'sqlserver://dbsync:My_Strong_Password1@localhost:1433/dialfire'
3. Sync continuously
./dbsync2 --a db_sync \
--c MY_CAMPAIGN_ID \
--ct MY_CAMPAIGN_TOKEN \
--url 'sqlserver://dbsync:My_Strong_Password1@localhost:1433/dialfire'
Notes:
- For a named instance, put the instance before the database name:
sqlserver://dbsync:My_Strong_Password1@localhost:1433/SQLEXPRESS/dialfire. - Numeric values are stored as
nvarcharon SQL Server (see Data types), so arithmetic needs an explicit cast:SELECT SUM(CAST(edit_time_sec AS float)) FROM transactions; - Column names contain
$, so they must be quoted with square brackets in queries:SELECT [$id], [$phone] FROM contacts; mssql://is accepted as an alias forsqlserver://(v1.20.2+).
Filtered example
Transfer all transactions from 01 February 2018 in the campaign MY_CAMPAIGN to a local running instance of Microsoft SQL Server. Only updates that begin with the prefix 'fc_' or 'qc_' in campaign stages and have been performed by a user are to be transferred.
dbsync2 --a db_sync --fm hi_updates_only --fp 'fc_,qc_' --c MY_CAMPAIGN_ID --ct MY_CAMPAIGN_TOKEN --s 2018-02-01 --url 'sqlserver://my_user:my_password@localhost:1433/sql_server_instance/my_database'
Running as a service
db_sync is meant to run permanently. A minimal systemd unit (/etc/systemd/system/dbsync2.service):
[Unit]
Description=Dialfire dbsync2
After=network-online.target postgresql.service
[Service]
Type=simple
User=dbsync
Environment=DBSYNC_C=MY_CAMPAIGN_ID
Environment=DBSYNC_CT=MY_CAMPAIGN_TOKEN
Environment=DBSYNC_URL=postgres://dbsync:my_password@localhost:5432/dialfire?sslmode=disable
ExecStart=/usr/local/bin/dbsync2 --a db_sync
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
sudo install -d -o dbsync -g dbsync /var/log/dbsync2 /var/opt/dbsync2
sudo systemctl daemon-reload && sudo systemctl enable --now dbsync2
sudo journalctl -u dbsync2 -f
The install -d line matters: the service user needs to write the log directory and the state directory, otherwise both silently fall back to that user's home (see Log and State Files).
Passing the credentials as DBSYNC_* environment variables keeps the token out of the process list. See also Docker Compose Deployment.
Command Line Options
dbsync2 --help prints this list with the built-in defaults and is always authoritative for the version you are running. Every option is a flag — there are no positional arguments — and every flag can be replaced by an environment variable named DBSYNC_ + the flag name in upper case (--ct → DBSYNC_CT).
Required
| Option | Value | Description |
|---|---|---|
--a |
webhook | db_init | db_update | db_sync |
Execution mode, see Execution Modes. |
--c |
campaign id | The campaign to export. |
--ct |
token | Sync Client token of that campaign, see Activation. |
--url |
URL | Target: database connection URL, or the webhook endpoint in mode webhook. |
Selecting what is exported
| Option | Default | Description |
|---|---|---|
--s |
mode-dependent | Start date, e.g. 2018-02-01 or 2018-02-01T14:30:00. The contact listing evaluates the date part only, so a time component does not narrow it down further. When omitted: db_init fetches the whole history, db_update starts one week ago, db_sync resumes from its saved state. |
--fm |
(no filter) | Transaction filter mode: updates_only for transactions of type update only, hi_updates_only for updates triggered by a human interaction only. |
--fp |
(no filter) | Only transactions from tasks with these prefixes, comma separated, e.g. 'fc_,qc_'. |
--cf |
(all fields) | Long form --contact_fields. Only create columns for these campaign fields, comma separated, * wildcard allowed, e.g. 'kd_*,email'. --cf none creates no campaign columns at all, leaving only the $ system columns. Matching is case-sensitive (v1.21.1+); a pattern that matches no field is reported as a warning. |
--cfx |
(nothing excluded) | Long form --contact_fields_exclude. Do not create columns for these campaign fields, same syntax as --cf, applied after it. |
Database target
| Option | Default | Description |
|---|---|---|
--tp |
(none) | Prefix for all table names, e.g. --tp acme_ creates acme_contacts, acme_transactions, … Lets several campaigns share one database. |
--d |
32 |
Maximum number of simultaneous database connections. Lower it if the database refuses connections, raise it if chanDatabaseUpdater keeps growing. |
--w |
32 |
Number of worker goroutines used for fetching and splitting. |
Webhook target
| Option | Default | Description |
|---|---|---|
--wr |
5 |
Rate limit in requests per second, shared between transactions and inbound calls. |
--wb |
2 × wr |
Burst limit. |
Sync timing (mode db_sync only)
| Option | Short | Default | Description |
|---|---|---|---|
--catchup_window |
--cw |
24h |
How far back the periodic catch-up re-lists changed contacts. |
--catchup_interval |
--ci |
12h |
How often the periodic catch-up runs. |
--live_lookback |
--ll |
2m |
Look-back window of the live poll. |
--live_interval |
--li |
1m |
How often the live poll runs. |
Go duration syntax (90m, 24h, 168h). Details and tuning advice in Sync Timing Options.
Logging and diagnostics
| Option | Default | Description |
|---|---|---|
--v |
off | Print all log messages to stdout instead of writing a log file. Also raises the log level to debug. |
--l |
4 |
Log level: 0 CRITICAL, 1 ERROR, 2 WARNING, 3 NOTICE, 4 INFO, 5 DEBUG. |
--p |
off | Enable the pprof profiling server on port 8080 (http://localhost:8080/debug/pprof/). |
--version |
— | Print version, commit and build date, then exit. |
--help |
— | Print the usage text with examples, then exit. |
Caveats and Operational Notes
Schema handling
-
The database must exist; dbsync2 creates only the tables in it. Tables and columns are created with
IF NOT EXISTSsemantics, so re-runningdb_initagainst a populated database is safe — rows are upserted, not duplicated. -
Every non-deleted campaign field becomes a column in
contacts. New campaign fields are picked up automatically:db_syncre-reads the field list once per hour and adds the missing columns. -
Columns are only ever added, never changed or removed. If a campaign field is deleted, its column stays behind (with its data). If a field changes its type — e.g. from number to text — the existing column keeps the old SQL type, which can cause insert errors for values that no longer fit. Drop or convert such a column manually.
-
Only one index is created:
contacts($id, $md5). There are no indexes on the join columns, so add your own before running reporting queries over large tables:CREATE INDEX idx_transactions_contact ON transactions ("contact_id"); CREATE INDEX idx_connections_transaction ON connections ("transaction_id"); CREATE INDEX idx_recordings_connection ON recordings ("connection_id"); CREATE INDEX idx_inbound_calls_contact ON inbound_calls ("contact_id"); -
The
contact_id/transaction_id/connection_idcolumns are logical references. NoFOREIGN KEYconstraints are created, and rows can arrive out of order, so a child row may briefly exist before its parent.
Campaigns with many fields
Every campaign field becomes its own column in contacts, and every DBMS limits
how wide one row may be. On MySQL/MariaDB (InnoDB, 16 KB pages) the limit is
8126 bytes and the sync aborts with:
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB
may help. In current row format, BLOB prefix of 0 bytes is stored inline.
What counts is the number of variable-length columns, not their declared
width: in the row-size check InnoDB reserves about 41 bytes for every varchar
and every text column alike, because it may have to store the value off-page.
Measured on MySQL 8.4 with the default DYNAMIC row format:
| Table content | Columns that still work |
|---|---|
varchar(50) columns only |
196 |
text columns only |
196 |
numeric columns only |
> 600 (no limit reached) |
dbsync2 contacts (22 $ columns + campaign fields) |
174 campaign fields, 175 fails |
So the 22 fixed $ columns consume 22 of roughly 196 available slots, leaving
about 174 campaign fields of text type. Campaign fields of type number
become numeric, which is fixed-length and does not count towards this budget —
a campaign made up mostly of number fields can have far more fields.
Use --cf / --cfx (v1.21.0+) to decide which fields become columns:
# only the '$' system columns, no campaign fields at all
./dbsync2 --a db_sync --c MY_CAMPAIGN_ID --ct MY_CAMPAIGN_TOKEN --cf none \
--url 'mysql://dbsync:my_password@localhost:3306/dialfire?tls=false'
# only the fields you actually report on (case-sensitive!)
--cf 'kd_*,email,street,zip'
# everything except a few large groups
--cfx 'notes_*,internal_*'
Both options only ever prevent columns from being created. Columns that already exist are never dropped automatically, so after narrowing the selection drop the leftovers yourself — dbsync2 will not recreate them:
ALTER TABLE contacts DROP COLUMN `notes_1`, DROP COLUMN `notes_2`;
Data for fields without a column is skipped on write, so no error is raised for the values that are no longer stored. Narrowing the selection is therefore enough to get a stalled sync running again — no change to the existing table is needed.
Dropping the leftover columns is still worth doing, and not only for tidiness:
a column that is no longer part of the selection is never written again, so it
keeps its last synced value for existing contacts while new contacts get NULL.
The column then holds a mix of stale and empty values, which is easy to
misinterpret in reports.
Other things worth knowing:
- Patterns are case-sensitive (since v1.21.1). Dialfire field names are
case-sensitive and mixed case is common — a campaign can contain
Case,IDandagent_idside by side — so--cfx 'id'does not excludeID. If a pattern matches no field of the campaign, dbsync2 logs a warning naming it, which is how a wrong capitalisation shows up. - Setting a field to hidden in the campaign does not exclude it. dbsync2
filters on
deletedonly and ignores the field state, so hidden fields still become columns. - Dropping columns without setting
--cf/--cfxdoes not help either: the next schema refresh adds them back and hits the same limit. Indb_syncthe schema is refreshed every hour, so the failure repeats. - Check the row format with
SHOW TABLE STATUS LIKE 'contacts'.DYNAMIC(the default since MySQL 5.7) is what makes the 196 columns above possible; with the olderCOMPACTformat eachtextcolumn reserves a 768 byte prefix inline and only 10 of them fit.ALTER TABLE contacts ROW_FORMAT=DYNAMICfixes that. - MySQL column names are not case-sensitive, so two campaign fields whose
names differ only in case (
kundeandKunde) cannot both become columns — the table creation fails withERROR 1060: Duplicate column name. PostgreSQL keeps them apart because dbsync2 quotes identifiers. If you hit this on MySQL, exclude one of the two with--cfx 'Kunde'. - PostgreSQL allows 1600 columns per table and SQL Server 8060 bytes per row, so both are affected in the same way, just at a different threshold.
Data types
- Timestamps are stored as strings (
varchar(50), UTC, ISO-8601-like), not as native date/time types. Comparisons work lexicographically for equal-length values; for date arithmetic cast explicitly, e.g.WHERE fired::timestamp > now() - interval '1 day'on PostgreSQL. - On SQL Server, numeric values are stored as
nvarchar(50), soSUM()/AVG()need aCAST(... AS float). On PostgreSQL and MySQL numbers usenumeric. - Custom campaign fields are created as
textand are not length-limited. The fixed$…system columns arevarchar(50)/varchar(100), and values longer than the column are silently truncated. - On PostgreSQL, identifiers longer than 63 bytes are shortened and given a deterministic 8-character hash suffix, so a very long campaign field name will not appear verbatim as a column name. The mapping is stable across runs.
- Nested values (objects/arrays) are stored as JSON text, and empty strings are written as
NULL.
Operations
- State: the resume timestamp lives in
/var/opt/dbsync2/{CAMPAIGN_ID}.json(fallback$HOME/.dbsync2/{CAMPAIGN_ID}.json) and is written every minute and on shutdown. Delete it to makedb_syncstart over from--s. - Restarts are cheap. dbsync2 upserts by primary key and compares MD5 hashes, so re-processing the same period does not duplicate or corrupt rows. After longer downtime either just start
db_sync(it resumes) or rundb_update --s <date>first. - Shutdown:
SIGINT/SIGTERMdrain the workers and save the state before exiting. Give the process a few seconds; killing it withSIGKILLloses at most the last minute of progress, which the next run re-fetches. - A wrong campaign id or token exits immediately with a
403on the API URL. That is a credential problem, not a network problem. - One process per campaign. Two processes syncing the same campaign into the same tables will fight over the same rows and the same state file. To collect several campaigns in one database, use one process each with distinct
--tpprefixes. - Bottlenecks are visible in the log every 30 seconds as
Channel lengths: chanDataSplitter=… chanContactFetcher=… chanDatabaseUpdater=…. A permanently growingchanDatabaseUpdatermeans the database is the limit (raise--d, add indexes, check disk); a growingchanDataSplittermeans too few workers (raise--w). - Clock skew matters: start dates and the resume timestamp are UTC and come from the local clock. Keep the host on NTP.
Documentation
¶
There is no documentation for this package.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package logging implements a logging infrastructure for Go.
|
Package logging implements a logging infrastructure for Go. |
|
spf13/pflag
Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
|
Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags. |
|
maps
Package maps provides reusable functions for manipulating nested map[string]interface{} maps are common unmarshal products from various serializers such as json, yaml etc.
|
Package maps provides reusable functions for manipulating nested map[string]interface{} maps are common unmarshal products from various serializers such as json, yaml etc. |
|
providers/env
Package env implements a koanf.Provider that reads environment variables as conf maps.
|
Package env implements a koanf.Provider that reads environment variables as conf maps. |
|
providers/posflag
Package posflag implements a koanf.Provider that reads commandline parameters as conf maps using spf13/pflag, a POSIX compliant alternative to Go's stdlib flag package.
|
Package posflag implements a koanf.Provider that reads commandline parameters as conf maps using spf13/pflag, a POSIX compliant alternative to Go's stdlib flag package. |
|
Package mapstructure exposes functionality to convert an arbitrary map[string]interface{} into a native Go structure.
|
Package mapstructure exposes functionality to convert an arbitrary map[string]interface{} into a native Go structure. |
|
Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
|
Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags. |