Keep two databases in sync with Compare Datasets
Airtable is the Primary. Supabase is the Secondary and has to mirror it. Each run reads both tables, sorts every record into In A only, Same, Different or In B only, and applies only the inserts and deletes that are needed. Run it again and nothing happens.
No personal info: every lead is invented, emails use example.com, phones use the fictional 555-01xx range, and the exported workflow has no credentials, base IDs or project URLs.
The puzzle asks you to show
-
The workflow executing successfullyPress “Execute workflow” below. Every node that receives items turns green.
-
The Primary database dataAirtable · Primary › Table 1 › Grid view, read by the Primary DB node.
-
Before & After of the Secondary databaseSupabase · Secondary. Both snapshots are captured when the run finishes.
| AName | ☎Phone Number | ◉Status | ||
|---|---|---|---|---|
omittedImportable workflow · 6 nodes
- In n8n, choose Import from file (or paste the JSON onto the canvas).
- Primary DB: select your Airtable credential (token scopes
data.records:read,schema.bases:read), then pick base Primary and table Table 1. - Secondary DB, Create/Replace Row, Delete Row: select your Supabase credential (service_role secret). The table is already set to
Secondary. - Create the Supabase table with
s56-secondary-table.sql(RLS on, one “Before” row). - Screenshot the Before table, execute the workflow, then screenshot the After table.
The four outputs, and what this workflow does with each
| Output | What lands there | Wired to |
|---|---|---|
| In A only | Airtable records with no identical Supabase row: new records, or the new version of an edited record | Create/Replace Row |
| Same | Pairs that match on every key and are equal once skipped fields are ignored | Nothing: already in sync |
| Different | Pairs that match on the keys but differ in another compared field | Nothing: every business field is a key, so this can't happen |
| In B only | Supabase rows nobody paired with: deleted records, or the old version of an edited record | Delete Row (id = {{ $json.id }}) |
Why an edit becomes a replace
The match uses all four fields: fields.Name↔Name, fields.Email↔Email, fields.Phone Number↔Phone Number, fields.Status↔Status.
A record only pairs with an exact copy of itself. When Jane's status changes, new-Jane comes out of In A only and old-Jane out of In B only.
That is why the reference screenshot's After table starts at id 2: the stale id 1 was deleted, not updated.
Why the field paths look different on each side
Airtable node v2.2 returns { id, createdTime, fields: { … } }, while Supabase returns a flat row. So Input A uses dot paths and Input B uses plain names.
After pairing, Compare Datasets deep-compares the whole objects, which never match with two different shapes. Fields to Skip Comparing removes
id, createdTime, fields, created_at and the key fields, so unchanged pairs reach Same. Turn the switch off in the studio to watch them fall into Different.
Traps
Plain field names with Airtable v2.2
Matching Email↔Email finds nothing, because the value is at fields.Email. Every run inserts every record again.
No skip fields
Unchanged rows show under Different. The sync is still correct because Different isn't wired, but the execution view is misleading.
Empty match field
Airtable leaves empty fields out of the API response. A missing key never pairs, so that row is deleted and re-created on every run. Make the fields required, or add a normalize step (?? '').
Supabase 50-row default
With Return All off, rows after 50 look missing and their Airtable twins are inserted again as duplicates.
Editing the mirror
Rows added straight into Secondary are In B only and get deleted. This is a one-way mirror: edit the Primary.
Going further
- Stable ids: store
airtable_id, match only onid↔airtable_id, and wire Different → Update row. Edits then update in place. - Automate: swap the manual trigger for a Schedule Trigger. The workflow is idempotent, so extra runs change nothing.
- Engine: the comparison on this page is a line-by-line port of n8n's
CompareDatasets/GenericFunctions.ts, checked against the original.
Sync scenario tests
The same scenarios that were run against n8n's own Compare Datasets source, now running on this page's engine.