How to Migrate a Relational Database Between Two Bubble.io Apps Using n8n (Step-by-Step)
Use n8n to clone Parent and Child data types between Bubble.io apps with full relationship data mapping.

TL;DR
Problem: Move parents (
data_type_1) and children (data_type_2) between Bubble apps while preserving the relation (parent list of children & child’s single parent).Approach: Read all parents from App-1, compare with App-2, create missing parents in App-2, fetch each parent’s children from App-1, create children in App-2, then patch parent records in App-2 to reference created children IDs.
Tool: n8n workflow (manual trigger → HTTP GET App1/App2 → dedupe → batch create → create children → merge → update relationships).
Challenges in relational database migration
Duplicate prevention - avoid creating the same parent twice in App-2.
Unique ID mapping - Bubble object
_idvalues differ between apps; you must map App-1 IDs → App-2 IDs to reconstruct relations.List vs single fields - parent often stores a list of child IDs, child stores a single parent reference. Both directions must be kept consistent.
API constraints & rate limits - Bubble APIs can be rate-limited; batch and sleep strategically.
Data shape differences - field names (or types) may differ; must transform payloads.
Atomicity - multi-step migrations can partially succeed; plan for resume/rollback (store mapping and logs).
Why use n8n for data migration?
No-code glue: HTTP nodes + code nodes let you call the Bubble REST API and transform payloads visually.
Batched processing: the splitInBatches node handles large datasets safely and avoids rate-limit bursts.
Custom logic: Code nodes for dedupe/formatting relations are simple JS.
Visibility & re-run: You can re-run failed batches and inspect inputs/outputs.
Extendability: Same pattern works to migrate between Bubble ↔ Supabase ↔ Xano, etc (just change endpoints/auth and payload shape).
Pre-requisite setup in Bubble (what to prepare before running the workflow)
Admin API keys (or API token) for both apps:
- Create API keys / set the proper endpoint access in Bubble (Settings → API if required). Ensure the token has create/read/patch permissions for the data types in question.
Enable Data API on both apps (Settings → API → check Expose Data API for each Data Type you’ll use).
CORS / IP - If you restrict IPs, add the n8n server IP or run n8n where it can reach Bubble.
Field mapping document - record field names in App-1 and App-2 for each data_type. (e.g.,
field_1,_variants_list_custom_data_type_2,field_name_text, etc.) Sample n8n JSON references names - confirm they match your bubble types.Backup - export both data sets (Data → App Data → Export) before running the migration.
Test environment - always run on staging apps first.
Decide mapping storage - either keep mapping in n8n run data or write a temporary mapping table in App-2 (recommended for large runs).
n8n Setup for data migration explained with an example
Use this JSON file as a blueprint
Note: replace app1domain.com and app2domain.com With your actual Bubble app API hostnames and set the credentials referenced in the JSON.
1) Manual trigger
Node:
When clicking ‘Execute workflow’(manualTrigger)Purpose: Start the migration manually so you can control the run.
2) Get all parents from App-1
Node:
Get data_type_1 data - App 1(HTTP Request GET →https://app1domain.com/api/1.1/obj/data_type_1)Important settings:
Authentication: HTTP Bearer (use your Bubble API token).
Keep default GET, no constraints for first full read (unless you want partial).
Output: JSON response with
response.resultsarray.
3) Get all parents from App-2 (to detect duplicates)
Node:
Get data_type_1 data - App 2(HTTP Request GET →https://app2domain.com/api/1.1/obj/data_type_1)Purpose: get existing records on the destination, so we create only the missing parents.
4) Filter missing parents (Code)
Node:
Filter list of Missing data in app2(Code node - JS)Code logic (from sampleJSON): it loads App1 & App2 results, extracts
field_name_textfrom App2 and filters App1 for entries not present in App2.Action: this returns items representing parents that need creation in App-2.
You must verify the key used to compare uniqueness (
field_name_textin the sample workflow). Replace with the real uniqueness field (e.g.,slug,email,name) if different.
Snippet used:
const app1 = $item(0).$node["Get data_type_1 data - App 1"].json.response.results;
const app2 = $item(0).$node["Get data_type_1 data - App 2"].json.response.results;
const app2Ids = app2.map(t => t.field_name_text);
const missing = app1.filter(t => !app2Ids.includes(t.field_name_text));
return missing.map(t => ({ json: { field_name_text: t.field_name_text } }));
5) Batch loop for creating missing parents
Node:
loop create data_type_1 in app2(SplitInBatches)Purpose: process missing parents in small batches. Suggested batch size: 10–50, depending on API limits.
Important: set batchSize appropriately and use wait between requests (or add a Wait node) to avoid rate limits.
6) For each missing parent, fetch the full parent from App-1 and fetch its children
Node (parallel outputs):
Fetch individual data_type_1 from app1- HTTP GET tohttps://app1domain.com/api/1.1/obj/data_type_1withconstraintsA query that finds the parent by the uniqueness field. The JSON query expression in the sample workflow is:constraints = JSON.stringify([{ key: "id_name_text", constraint_type: "equals", value: $json.id_name_text }])Replace
id_name_textwith your actual unique key.Create data_type_1 in app2- HTTP POST tohttps://app2domain.com/api/1.1/obj/data_type_1with a JSON body mapping the fields from App-1 response to App-2 fields. Sample workflow uses:{ "field_1": {{ $json["response"]["results"][0]["field_1"] }}, "field_2": "{{ $json['response']['results'][0]['field_2'] }}", "list_of_field_3": {{ JSON.stringify($json["response"]["results"][0]["list_of_field_3"]) }} }Update
field_1/field_2/list_of_field_3to match actual fields.
Why are both parallel? You create the new parent in App-2 and also fetch the parent’s children (next step) - both outputs are merged later.
7) Fetch child data for that parent in App-1
Node:
Fetch child data (data_type_2) from app1- HTTP GET tohttps://app1domain.com/api/1.1/obj/data_type_2with a constraintinon parent’s_idlist (see sample JSON).Example query in sample workflow:
constraints = JSON.stringify([{ key: "_id", constraint_type: "in", value: $json["response"]["results"][0]["_variants_list_custom_data_type_2"] }])Replace the key
"_variants_list_custom_data_type_2"with the actual parent → child list field in App-1.
8) Merge parent-create output + fetched children
- Node:
Merge- combines theCreate data_type_1 in app2output and theFetch child dataoutput so the workflow has both the new parent’s App-2 result and the list of App-1 children to create.
9) Create child records in App-2
Node:
Create data_type_2 in app2- HTTP POST tohttps://app2domain.com/api/1.1/obj/data_type_2Payload example from sample JSON:
{ "_field_1": $json.id, "isactive_boolean": $json["response.results"]["isactive_boolean"], "field_2": $json["response.results"]["field_2_text"] }_field_1should store the parent reference (use the new parent id returned from App-2 - ensure you capture it).If the child needs to reference the parent via Bubble relation, send the correct field (usually the parent’s
_idorlistfield format expected by Bubble.
Important: the code in the sample workflow sets field_id via a Set node, which stores the created record ID for later mapping. Make sure you capture and persist App1_parent_id -> App2_parent_id the mapping somewhere (either in a temporary data type in App-2 or as a file). The Set node in the sample flow does field_id = $json.id - Adapt the name to your types. Data Migration from One Bubble …
10) Merge App-2 child & parent rows into relation format
Node:
Merge app2 data_type_2 and data_type_1 in single json- combines created child responses and created parent responses into one stream.Node:
Change format of data for relation(Code node) - the JS groups children by parent id and creates an object like:{ json: { data_type_1_id: dataType1Id, data_type_2_ids: [ ... ] } }This is exactly what you need to PATCH the parent record in App-2 with the list of child IDs.
Code used in sample workflow (copy/paste):
const items = $input.all();
const grouped = {};
for (const item of items) {
const dataType1Id = item.json.data_type_1_id;
const dataType2 = item.json.data_type_2;
if (!grouped[dataType1Id]) { grouped[dataType1Id] = new Set(); }
grouped[dataType1Id].add(dataType2);
}
const output = Object.entries(grouped).map(([dataType1Id, dataType2Set]) => ({
json: {
data_type_1_id: dataType1Id,
data_type_2_ids: Array.from(dataType2Set),
}
}));
return output;
(Modify data_type_1_id / data_type_2 keys to match your payload names.)
11) PATCH parent in App-2 to update the relationship (list of children)
Node:
Update Relationship between data_tytpe 1 and data_type 2 (App 2)- HTTP PATCH tohttps://app2domain.com/api/1.1/obj/data_type_2/{{ $json.data_tyep_1_id}}(note: fix typodata_tyep_1_id→data_type_1_id).Body example from JSON:
{ "_variants_list_custom_data_type_2": $json.variant_ids }Replace
_variants_list_custom_data_type_2with your parents’ list field name in App-2 andvariant_idswith the array produced by the previous code node (e.g.,data_type_2_ids).
Important fixes & checks
- Ensure mapping: when creating child records in App-2, capture their returned
idand attach the created child ID to the mapping item so the code node can group them.
Practical tips, gotchas & recommended config
Batch size: Use conservative sizes (10) and increase only after testing. Add a small Wait (1s) after each POST if you hit rate limits.
Logging: store each created mapping row in a temporary Bubble data type
migration_mappingwith fieldsapp1_id,app2_id,type,status. This helps resume if something fails.Idempotency: make API POSTs idempotent by checking uniqueness before creating (as your code does by listing App-2 first). Also, consider creating a unique
external_idfield to catch duplicates later.Field type conversions: arrays/lists must be
JSON.stringify(...)in your POST bodies (workflow already does that in examples).Testing flow: run with 3–5 sample parents first, inspect results, then full run.
Rollback plan: if needed, delete the created App-2 records using the stored mapping.
How does this logic extend beyond Bubble.io?
The pattern is generic:
Read source rows via API (App-1 / Supabase / Xano).
Read destination existing rows to skip duplicates.
Create missing parents on the destination.
For each parent, fetch children from the source and CREATE them on the destination.
Build a mapping of
source_id -> dest_id.PATCH parents on the destination to attach children IDs.
Change points when migrating to other DBs:
Auth: use Bearer/token for Bubble; Supabase uses service_role key; Xano uses its token - n8n HTTP nodes handle each.
Endpoints & payload shapes: adapt POST/patch body to each API’s contract (Supabase expects POST
/rest/v1/<table>with headersapikey+AuthorizationandPrefer: return=representationto get created IDs).Batching & bulk insert support: Supabase supports bulk inserts - use that to speed up child creation (but still capture returned IDs).
Relationship formats: relational DBs use foreign keys rather than a list of IDs. For relational DBs, you will set a foreign key on the child row to the parent ID (simpler than patching the list on the parent).
Frequently asked questions (FAQs)
Q: Can I migrate large datasets (10k+ records) with this n8n flow?
A: Yes — but use batching, temporary mapping storage, and run in stages. Monitor rate limits and use splitInBatches with conservative sizes (e.g., 50).
Q: How do I ensure no duplicates in App-2?
A: Compare a unique field (slug/email/external_id) from App-1 with App-2 before creating. Optionally store an external_id On App-2 to mark the origin.
Q: Do I need to write code?
A: Minimal JS is used inside n8n Code nodes for grouping/deduping - copy/paste snippets provided in this guide. No external scripts required.
Q: What if field names differ between apps?
A: Map fields explicitly in the Create POST node bodies; use JSON.stringify() for lists and use Code nodes to reshape data.
Q: How to resume a partially failed run?
A: Keep a migration_mapping (or use the mapping Set node) that stores successful app1_id -> app2_id. Filter already migrated parents out on rerun.
Q: Is it safer to create children first or parent first?
A: Create the parent first (to get its App-2 ID), then create children attaching that parent id. After all children are created, patch the parent with a list if your data model requires it.
Quick checklist before pressing Execute
Export & backup both app data.
Confirm API tokens for both App-1 and App-2 in the n8n credentials.
Update all domain placeholders
app1domain.com/app2domain.com.Adjust the uniqueness key used in the dedupe code (
field_name_textin your flow).Fix typos in node templates (
data_tyep_1_id→data_type_1_id).Set safe
batchSize(start small).Add mapping persistence (recommended).
Run 3–5 sample parents and validate.





