Migrating Bubble Data to Postgres: The Complete Reference
How to move every Data Type from Bubble to Postgres without corrupting data. Idempotent imports, soft-delete handling, option sets, recursive foreign keys, reconciliation.
Data migration is where most Bubble-to-code migrations actually fail. The code rebuild gets all the attention; the data import gets two paragraphs in a SOW. Then on cutover day, three Data Types don't reconcile, a thousand records have wrong references, and the team spends a month patching up the mess by hand.
This is a reference for how to do it properly. It assumes you've got the target architecture in place - typically Postgres on Supabase with Drizzle ORM schema. The patterns work for any Postgres target.
The four invariants of a good data migration
Before talking about Bubble specifically: every data migration should hold to four invariants.
1. Idempotent
Running the import twice should produce the same result as running it once. If you can't safely re-run, you can't recover from partial failures, and you can't test the import against staging before running it against prod.
In practice this means using upserts (INSERT ... ON CONFLICT (...) DO UPDATE) instead of inserts, and using a stable foreign identifier (the Bubble unique ID) as the conflict target.
2. Chunked
Bubble's data API rate-limits you and times out on large queries. The import has to page through records in batches (we use 100-200 per batch typically). Each batch should commit independently so a network blip doesn't roll back the whole import.
3. Resumable
If the import fails halfway through, you need to know exactly where it failed and pick up from there. We use a small import_progress table that tracks which Bubble unique IDs have been imported into which target table. The next run skips what's already done.
4. Reconciled
After the import, you need automated checks that prove the data is correct. Not "looks right" - actually correct. That means comparing row counts per table, checking foreign-key integrity, and spot-checking sample records against the Bubble source.
If you can't show me a reconciliation report at the end, you didn't migrate the data. You moved it.
The Bubble side: how to read the source
Bubble exposes data via its Data API. The API supports:
- Listing records of a given Data Type, paged via
cursorandlimit - Filtering with constraints
- Reading individual records by
_id(the stable Bubble unique ID)
In practice, you need three pieces of information per Data Type to design a clean import:
| Source detail | Why it matters | Where to find it |
|---|---|---|
| Total record count | Sizing the import, defining "done" | Bubble Data tab → filter view count |
| Field list with types | Mapping to Postgres columns | Bubble Data tab → field inspector |
| Relationships to other types | Ordering the imports | Manual audit during discovery |
The third one is the trap. Bubble doesn't enforce referential integrity, so it's common to find records pointing at other records that no longer exist. The migration has to decide what to do with those orphans - drop, retain with NULL, or flag for manual review.
The migration order matters
You can't migrate User-referenced records before Users exist. So the order of imports matters. The rule we use:
Migrate types in dependency order. A type can only be imported after every type it references has been imported.
In practice, this means doing a topological sort of the Data Types based on the relationships you mapped during discovery. Most apps end up with 4-6 "tiers":
- Tier 1: Pure leaf types with no foreign references. Often the simplest enums and metadata: Country, Currency, RoleType.
- Tier 2: User and Organisation (or whatever your top-level multi-tenant entities are).
- Tier 3: Core domain objects (Bookings, Properties, Orders, etc.).
- Tier 4: Junction records and history (Audit logs, BookingEvents, Notifications).
- Tier 5+: Anything left.
If you find a cycle in the dependency graph - Type A references Type B and vice versa - you need a two-phase import for that pair: insert all Type A rows with NULL for the Type B reference, insert all Type B rows, then go back and update Type A.
The Bubble-specific gotchas
Soft-deletes hidden in a "Deleted?" field
Many Bubble apps implement soft-delete by adding a Deleted? yes/no field to Data Types. If you carry these through to Postgres as-is, you've now polluted your new database with soft-deleted records.
Decide during discovery: are you carrying soft-deletes forward, or filtering them out at import time? Both are valid. Picking is required.
Option sets and their "display" fields
Bubble Option Sets are basically enums with a display name and a hidden internal key. The import has to decide whether to migrate option-set values as a foreign-key reference to a lookup table, as a Postgres enum type, or as a plain text column. We default to lookup tables for anything that might grow, enums for genuinely fixed sets.
The trap: if you forget to migrate the option set itself before migrating the records that reference it, you end up with a column full of string values pointing nowhere.
Recursive Data Types (Thing references Thing)
A few Data Types reference themselves - tree structures, parent/child relationships, threaded comments. These need the two-phase import we mentioned above. Bubble doesn't warn you about cycles; you have to spot them yourself.
Date/time inconsistencies
Bubble stores dates in UTC but displays them in the user's locale. Some apps end up with Date fields that contain a date string in the local format (because someone wrote a custom workflow that wrote it that way). The import has to be defensive about parsing.
File and image references
Bubble files are URLs to Bubble's S3 bucket. You have three choices for the migration:
- Reference the existing URLs. Cheapest. Cleanest. The risk is you're permanently dependent on Bubble's CDN. Acceptable if you keep the Bubble app as a read-only fallback indefinitely.
- Re-upload to your own storage. Most robust. Costs time and bandwidth but gives you full control.
- Lazy-migrate on first read. A hybrid. New uploads go to your storage; old files migrate on first access. Complex but minimises upfront cost.
Long text fields with embedded HTML
Some Bubble apps store rich text as HTML in a Text field. Make sure the receiving Postgres column is TEXT not VARCHAR(N), and that your sanitisation rules survive the migration unchanged.
The reconciliation step
After every import run, generate a reconciliation report with these checks at minimum:
- Row counts per Data Type - Bubble source count vs Postgres target count. Mismatches need investigation.
- Foreign key integrity - count of rows where the referenced parent doesn't exist. Should be zero.
- Spot-check sample records - randomly pick 10-20 records per type and compare the field-level data.
- Sum or hash of numeric fields - if you have a "total" or "amount" field, the sum of all rows should match between Bubble and Postgres.
We've never run a migration where the reconciliation report came back perfect on the first try. The point isn't perfection - it's surfacing the discrepancies while you can still fix them, not after cutover.
How long it takes
For a typical mid-stage Bubble app (10-30 Data Types, 50K-500K total rows), data migration is usually 1-2 weeks of focused work:
- 2-3 days designing the import scripts and dependency order
- 2-3 days running and reconciling against staging
- 1-2 days for the final cutover-window import against production
- A few days for any post-cutover backfill work
This is one of the six phases of our Bubble migration playbook. The full migration runs 8-16 weeks end-to-end.
What to do next
If you're planning a migration, start with our discovery audit - the data model and dependency analysis is the input to the data migration.
If you want a senior pair of eyes on your specific app before you commit, request a free Bubble app audit.
Read next: Bubble to Next.js migration guide and Slice-by-slice vs big-bang migration strategies.
Got a Bubble or Canvas app you’d like a second pair of eyes on?
30-minute discovery call. We’ll look at your app live and tell you honestly what we’d do next.
Or grab the Bubble migration playbook PDF.