Build reliable database synchronization pipelines with n8n across PostgreSQL, Supabase, and MySQL using idempotent upserts and CDC triggers.

CEO of Sharcon LLC
15+ years in marketing and development. Leading a team of 30+ professionals at Sharcon.
Configuring an n8n database sync workflow requires establishing change detection triggers, data transformation mappings, and idempotent database write operations. Using n8n, developers can synchronize record states across PostgreSQL, Supabase, and MySQL databases without writing complex custom ETL software. Achieving strict data integrity demands robust polling or Change Data Capture (CDC) strategies, explicit upsert queries utilizing ON CONFLICT clauses, and isolated error handling sub-workflows for failed payload processing.
Synchronizing data across heterogeneous databases using n8n requires isolating trigger ingestion, transformation logic, destination writing, and error logging into distinct pipeline stages.
+---------------------------------------------------------------------------------+
| N8N DATABASE SYNC ARCHITECTURE |
+---------------------------------------------------------------------------------+
| |
| [ Source Database ] |
| (PostgreSQL / Supabase / MySQL) |
| | |
| +---> Change Trigger (CDC Webhook / Timestamp Polling) |
| | |
| v |
| [ n8n Workflow Engine ] |
| +---------------------------------------------------------------------------+ |
| | Node 1: Ingest Payload & Validate Schema Structure | |
| | Node 2: JavaScript Code Node (Data Normalization & Mapping) | |
| | Node 3: Target Database Node (Idempotent Upsert / ON CONFLICT) | |
| +---------------------------------------------------------------------------+ |
| | | |
| Success v Error v |
| [ Destination Database ] [ Dead-Letter Queue / DB ] |
| (PostgreSQL / MySQL / Supabase) (Error Log & Alerting) |
| |
+---------------------------------------------------------------------------------+
Connecting source and target database instances within n8n involves choosing between incremental polling and event-driven Change Data Capture (CDC).
updated_at Timestamp Tracking)Incremental polling queries the source database periodically for records created or updated since the last execution run.
-- Source Database Query (PostgreSQL / MySQL)
SELECT
id,
first_name,
last_name,
email,
status,
updated_at
FROM users
WHERE updated_at > :last_sync_timestamp
ORDER BY updated_at ASC
LIMIT 1000;
To persist the cursor timestamp between executions, use n8n's static workflow data store in a JavaScript Code node:
// Preserve sync timestamp across workflow runs using static data store
const staticData = $getWorkflowStaticData('global');
const lastSync = staticData.lastSyncTimestamp || '1970-01-01T00:00:00.000Z';
// Capture current execution timestamp
const currentSync = new Date().toISOString();
// Update static data for the next execution cycle
staticData.lastSyncTimestamp = currentSync;
return $input.all().map(item => {
item.json._lastSyncUsed = lastSync;
return item;
});
For sub-second data synchronization, use database triggers or Change Data Capture engines that emit HTTP webhooks directly into an n8n Webhook node.
Configure a Supabase Database Webhook to emit payload JSON on INSERT or UPDATE events:
{
"type": "UPDATE",
"table": "customers",
"schema": "public",
"record": {
"id": "usr_99812",
"email": "alex@example.com",
"tier": "enterprise",
"updated_at": "2026-10-08T14:30:00Z"
},
"old_record": {
"id": "usr_99812",
"email": "alex@example.com",
"tier": "pro",
"updated_at": "2026-10-01T09:00:00Z"
}
}
For self-hosted PostgreSQL instances, you can create a database trigger function that sends notifications when rows change:
-- Create trigger function for change notification
CREATE OR REPLACE FUNCTION notify_user_change()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'user_updates',
json_build_object(
'action', TG_OP,
'record', row_to_json(NEW)
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Attach trigger to target table
CREATE TRIGGER trigger_user_change
AFTER INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION notify_user_change();
An n8n microservice or custom listener script can listen to the user_updates channel and dispatch HTTP post calls to n8n webhooks instantly.
For custom database pipeline design and backend server integrations, review our backend API development services.
Data schemas between disparate databases rarely match directly. Fields must be mapped, sanitized, and type-cast using n8n JavaScript Code nodes.
// Transform source PostgreSQL/Supabase record to target MySQL/PostgreSQL schema
const transformedItems = [];
for (const item of $input.all()) {
const record = item.json.record || item.json;
// Normalize timestamps to standard ISO string format
const formattedUpdatedAt = record.updated_at
? new Date(record.updated_at).toISOString()
: new Date().toISOString();
// Serialize nested JSON objects for databases expecting text/JSON columns
let metadataString = '{}';
if (record.metadata && typeof record.metadata === 'object') {
metadataString = JSON.stringify(record.metadata);
} else if (typeof record.metadata === 'string') {
metadataString = record.metadata;
}
// Convert array attributes into delimited strings or clean arrays
const userTags = Array.isArray(record.tags)
? record.tags.join(',')
: (record.tags || '');
transformedItems.push({
json: {
external_id: String(record.id),
full_name: `${record.first_name || ''} ${record.last_name || ''}`.trim(),
user_email: String(record.email || '').toLowerCase().trim(),
account_status: record.status || 'pending',
tag_list: userTags,
custom_metadata: metadataString,
synced_at: new Date().toISOString(),
source_updated_at: formattedUpdatedAt
}
});
}
return transformedItems;
Preventing duplicate key violations and data overwrites requires constructing explicit upsert statements in target database nodes.
ON CONFLICT)In the PostgreSQL node, select the Execute Query action and execute an ON CONFLICT statement:
INSERT INTO target_customers (
external_id,
full_name,
user_email,
account_status,
tag_list,
custom_metadata,
synced_at,
source_updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
ON CONFLICT (external_id)
DO UPDATE SET
full_name = EXCLUDED.full_name,
user_email = EXCLUDED.user_email,
account_status = EXCLUDED.account_status,
tag_list = EXCLUDED.tag_list,
custom_metadata = EXCLUDED.custom_metadata,
synced_at = EXCLUDED.synced_at,
source_updated_at = EXCLUDED.source_updated_at
WHERE target_customers.source_updated_at < EXCLUDED.source_updated_at;
Note: The conditional clause
WHERE target_customers.source_updated_at < EXCLUDED.source_updated_atensures that out-of-order webhook events cannot overwrite newer destination data with older state.
ON DUPLICATE KEY UPDATE)In the MySQL node, execute an equivalent ON DUPLICATE KEY UPDATE query:
INSERT INTO target_customers (
external_id,
full_name,
user_email,
account_status,
tag_list,
custom_metadata,
synced_at,
source_updated_at
) VALUES (
{{ $json.external_id }},
{{ $json.full_name }},
{{ $json.user_email }},
{{ $json.account_status }},
{{ $json.tag_list }},
{{ $json.custom_metadata }},
{{ $json.synced_at }},
{{ $json.source_updated_at }}
)
ON DUPLICATE KEY UPDATE
full_name = VALUES(full_name),
user_email = VALUES(user_email),
account_status = VALUES(account_status),
tag_list = VALUES(tag_list),
custom_metadata = VALUES(custom_metadata),
synced_at = VALUES(synced_at),
source_updated_at = VALUES(source_updated_at);
For custom database synchronization setups and complex multi-source ETL pipelines, explore our system integration services.
Network blips, database lock timeouts, and schema validation failures will occur in production. A resilient n8n sync pipeline handles errors gracefully without dropping data.
+-------------------+
| Primary Sync Node |
+-------------------+
|
+-----------------+-----------------+
| |
Success v Failure v
+-----------------------+ +-------------------------------+
| Destination DB Written| | Error Trigger Node |
+-----------------------+ | (Captures Execution Payload) |
+-------------------------------+
|
v
+-------------------------------+
| Dead-Letter Queue (DLQ Table) |
| (Logs Payload & Stack Trace) |
+-------------------------------+
|
v
+-------------------------------+
| Replay Sub-Workflow |
| (Re-Executes Failed Sync) |
+-------------------------------+
Inside n8n node settings, enable built-in retry mechanics for target database connections:
Continue On Fail or Error Trigger).Create a secondary workflow dedicated to logging error payloads to a dead_letter_queue table and notifying engineering teams.
INSERT INTO sync_dead_letter_queue (
workflow_id,
workflow_name,
failed_node,
error_message,
payload_json,
failed_at
) VALUES (
$1, $2, $3, $4, $5, NOW()
);
Once temporary target database issues are resolved, run an automated replay workflow in n8n that reads records from sync_dead_letter_queue where status = 'pending', re-executes the upsert query, and updates status = 'resolved'.
Synchronizing large database tables containing millions of rows requires optimizing batch sizes and index strategies.
Rather than executing single database inserts sequentially in a loop, process incoming arrays using n8n's Split In Batches node.
Ensure that all fields used in ON CONFLICT clauses, WHERE timestamp filters, or join conditions possess database indexes on both source and target servers:
-- Create unique index for idempotent conflict resolution
CREATE UNIQUE INDEX IF NOT EXISTS idx_target_cust_ext_id ON target_customers(external_id);
-- Create timestamp index for high-speed incremental polling
CREATE INDEX IF NOT EXISTS idx_source_users_updated_at ON users(updated_at);
To build analytics dashboards that track database sync latency and system performance, explore our custom dashboard development services.
Building database synchronization workflows with n8n requires avoiding several operational pitfalls:
Executing incremental polling queries (WHERE updated_at > :last_sync) without an index on updated_at forces full table scans on the source database, causing query timeouts as the database grows.
Executing ON CONFLICT (external_id) in PostgreSQL without a UNIQUE constraint or unique index on external_id causes database execution errors. Always verify schema constraints before running upsert workflows.
Webhook payloads can arrive out of order due to network latency. Failing to include timestamp comparison clauses (WHERE target.source_updated_at < EXCLUDED.source_updated_at) can cause older updates to overwrite newer database states.
Never hardcode database strings or secrets inside Code nodes or query parameters. Always use n8n's encrypted Credential Vault to store database connection details.
Yes. When deployed in Redis Queue Mode with worker nodes and optimized batch queries, n8n can process thousands of database records per second. For multi-million record initial bulk loads, standard ETL bulk copy tools (such as pg_dump or mysqldump) should be paired with n8n for real-time incremental updates.
n8n prevents duplicates by combining schema unique constraints with idempotent upsert queries (ON CONFLICT in PostgreSQL/Supabase or ON DUPLICATE KEY UPDATE in MySQL).
Yes. Bidirectional sync requires setting up workflow pipelines in both directions. To avoid infinite loops, each workflow must exclude updates triggered by the sync process itself (e.g., filtering out updates where updated_by = 'n8n_sync_service').
If the target database goes offline, n8n's retry settings attempt reconnects. If retries are exhausted, the workflow routes the execution payload to a dead-letter queue or error trigger, allowing engineers to replay failed sync tasks once database connectivity is restored.
Building reliable cross-database synchronization pipelines requires rigorous architecture, schema handling, and error-recovery engineering. Sharcon designs resilient n8n automation workflows, multi-database sync engines, and custom API pipelines for growing companies.
To build a secure, real-time data synchronization pipeline, explore our custom system integration services.

CEO of Sharcon LLC
15+ years in marketing and development. Leading a team of 30+ professionals at Sharcon.