Skip to main content

Command Palette

Search for a command to run...

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.

Updated
10 min read
How to Migrate a Relational Database Between Two Bubble.io Apps Using n8n (Step-by-Step)

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 _id values 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.results array.

3) Get all parents from App-2 (to detect duplicates)

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_text from 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_text in 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 to https://app1domain.com/api/1.1/obj/data_type_1 with constraints A 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_text with your actual unique key.

    • Create data_type_1 in app2 - HTTP POST to https://app2domain.com/api/1.1/obj/data_type_1 with 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_3 to 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 to https://app1domain.com/api/1.1/obj/data_type_2 with a constraint in on parent’s _id list (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 the Create data_type_1 in app2 output and the Fetch child data output 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 to https://app2domain.com/api/1.1/obj/data_type_2

  • Payload example from sample JSON:

      {
        "_field_1": $json.id,
        "isactive_boolean": $json["response.results"]["isactive_boolean"],
        "field_2": $json["response.results"]["field_2_text"]
      }
    
    • _field_1 should 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 _id or list field 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 to https://app2domain.com/api/1.1/obj/data_type_2/{{ $json.data_tyep_1_id}} (note: fix typo data_tyep_1_iddata_type_1_id).

  • Body example from JSON:

      {
        "_variants_list_custom_data_type_2": $json.variant_ids
      }
    

    Replace _variants_list_custom_data_type_2 with your parents’ list field name in App-2 and variant_ids with 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 id and attach the created child ID to the mapping item so the code node can group them.
  • 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_mapping with fields app1_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_id field 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:

  1. Read source rows via API (App-1 / Supabase / Xano).

  2. Read destination existing rows to skip duplicates.

  3. Create missing parents on the destination.

  4. For each parent, fetch children from the source and CREATE them on the destination.

  5. Build a mapping of source_id -> dest_id.

  6. 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 headers apikey + Authorization and Prefer: return=representation to 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_text in your flow).

  • Fix typos in node templates (data_tyep_1_iddata_type_1_id).

  • Set safe batchSize (start small).

  • Add mapping persistence (recommended).

  • Run 3–5 sample parents and validate.