July 2026
All articlesMigrating data out of Bubble goes far beyond CSV export or the Data API. The hard part is rebuilding schema, files, auth, Privacy Rules, and syncs correctly in Supabase.
If you are reading this, you probably want to move out of Bubble, and you have already realized where the migration gets serious: the data.
Getting the data out is the easy part, whether through CSV exports or the Data API. However, a real Bubble to Supabase migration still has to:
That is the real migration work: rebuilding those parts correctly in Supabase so the data model is usable, relational, and no longer dependent on Bubble.
Bubble does not make it difficult to export your data. You can do that through manual CSV exports or through the Data API. However, once the data is out, the real work begins.
When migrating to Supabase, you first need to define how each Bubble field maps into Postgres, and you quickly realize that many Bubble field types need to be transformed or re-modeled before they fit a real relational schema.
Bubble does not hand you Postgres-ready values. Fields come back in shapes that have to be re-modelled before they fit a column. That is real transformation work, not direct type conversion.
{"address":"R. Augusta 1, Lisboa",\n "lat":38.7107,"lng":-9.1393}geographic address - structured, never a plain string
address jsonb
or its own table, if you query by it
7948800000
date interval - milliseconds, not an SQL interval
92 days
interval
Option Sets export as display values only. The attributes behind the option are simply not in the export, so the rest of the structure has to be rebuilt separately.
"Status": "Active"
the display label, and nothing behind it
"status_id": "cncsU"
a foreign key to an option set table that keeps all the attributes
A list of things is not a relational column. If an orders table contains a list of products or tags, you either keep a weak array of IDs or build the proper join table yourself.
"Products": ["1707x8","1707x9","1707x12"]
opaque ids in a single cell
order_products (order_id, product_id)
a real join table you build yourself
Bubble sends relationship data as text. It is on your side to reconstruct the real foreign key constraints in Postgres.
"User": "1690x22"
a text field that happens to point at a User
user_id uuid references users(id)
a real foreign key constraint, enforced by the database
At that point, you are not exporting data anymore. You are redesigning how Bubble concepts become a real relational schema.
And even the extraction itself can fail. Bubble's Data API has a 50,000-row request-scope limit, so extraction logic must account for truncated result sets. If it does not, you can stop early and migrate incomplete data.
Files and images are a separate migration problem.
When those fields appear in the export, you are not getting the actual asset. You are getting a URL pointing to Bubble storage.
And yes, if you only have a few images to export, you can handle that manually. However, for most apps, that quickly becomes completely unreasonable.
And even if Bubble gave you a bulk download button for storage, that would not solve the problem. You would still need to move those files into new storage and update every reference across your migrated data.
To migrate them properly, you have to:
So this becomes a real migration problem. If you ignore it, your assets are still on Bubble, and you lose them once you stop paying.
| id | name | profile_picture |
|---|---|---|
1715...500 | Alice | cdn.bubble.io/... supabase.co/... |
Users are a good example of how much Bubble abstracts away from you.
The User table looks like a normal data type, but Bubble hides most of the authentication layer behind it. When you export the User table, you only get user data rows, not a ready-to-use auth system.
In a Supabase migration, that means migrating the Bubble user rows into Postgres, creating real auth users in Supabase Auth, and linking those auth identities back to the migrated user records.
Bubble also does not expose user passwords through the Data API. So even after the users are migrated, you still need a controlled sign up flow, such as a password reset, invitation flow, or magic link.
That is why user migration is not just exporting the User table. You also have to rebuild the auth layer that Bubble kept out of sight.
Bubble User
one exported row
public.users
your data row
auth.users
real Supabase identity
Option Sets are not real database tables. They are static app data, and many of them carry useful attributes of their own.
In exports, references to those Option Sets come through only as display values. You get something like paid or active, but not the option id or the rest of the structure behind it.
| id | total | status |
|---|---|---|
1716...233 | 840.00 | Paid |
Only the display label is in the export. The option id and attributes are missing.
So if a record points to an Option Set that also contains a color, sort order, final-state flag, or other attributes, that information is easy to lose unless you extract and rebuild it separately.
What Order Status actually holds in Bubble
Only the display value survives the export. Every other attribute is dropped.
In a real Supabase migration, important Option Sets usually need to become real Postgres tables, or explicit constraints if they have no additional attributes, so the structure survives outside Bubble.
| id | label | sort_order | color | is_final |
|---|---|---|---|---|
cncsP | Pending | 1 | #f59e0b | false |
cncsA | Paid | 2 | #10b981 |
Privacy Rules are one of the easiest parts to neglect in a Bubble database migration, and one of the riskiest if they are handled incorrectly.
You have a few options. You can rebuild them as PostgreSQL Row Level Security policies, or you can block direct database access and enforce access rules in your backend. However, even if you choose the second approach, the first step is still to make those rules explicit. In practice, that usually means modeling them clearly at the database level first, and only then deciding what should remain in RLS and what should move into backend enforcement.
This also depends on how your new system is structured. For example, it depends on how the new database is modeled, especially your users table, auth setup, ownership fields, and relationship structure. So this is not just a security task. It is also part of the data model redesign.
Ideally, you would migrate the data once and switch to the new system immediately.
In practice, these migrations almost always take longer than planned. You usually extract the data first, then spend time rebuilding the new system around it, and only later do a final sync before the switch. And during that period, more intermediate syncs are usually needed.
What does that mean? Until the final switch to the new Supabase-backed app, you need incremental syncs for:
Without that, every new iteration starts pushing you back toward a full rebuild, which quickly becomes much more expensive and fragile.
What a real migration usually looks like
1. Initial load
You extract the current Bubble data and load the first version into Postgres.
2. Build period
While you rebuild the product, Bubble keeps changing underneath you.
Moving from Bubble to Supabase is hard because export only solves the first layer of the problem.
After extraction, you still need to:
Bubble export is the beginning of the migration, not the migration itself.
If you want help with that process, this is exactly what unbub.dev is built for: handling the full migration, from Bubble data extraction to a working Supabase backend.
Self-serve tools or a done-for-you service. Your app stays live while we move everything.
URL column updated in place: same row, new storage location
false |
cncsF | Refunded | 3 | #64748b | true |
foreign key - rows reference the option, not its label
3. Final sync + switch
You sync the delta, confirm the new system, and only then move writes off Bubble.
Without incremental syncs, the middle phase keeps pushing you back toward a full restart.