How to get your data out of Bubble
Three ways out, the four data shapes that will not survive the trip, and why you should run this even if you are staying.
Lucian Lutas
Your data is yours and it is gettable. That is worth saying plainly, because the question usually arrives with an edge of panic to it, and the honest answer is that nobody is holding your records hostage.
What is genuinely difficult is not the export. It is the shape. Bubble's database is not a relational database with the relationships written down, and the gap between what you export and something Postgres will accept is where the work is.
This is the procedure, whether you are migrating, taking a backup you control, or just want to know you could leave.
Three ways out, and when each is right
CSV export from the editor. One data type at a time, from the Data tab. Fine for a look, fine for small types, and the least useful of the three for an actual migration, because it flattens relationships into text and gives you no guarantee about what happened to fields that were empty.
The Data API. Paginated JSON over HTTP, one endpoint per data type, and the option to filter and sort. This is the one to use for anything real. It returns the field names as the API knows them rather than as the editor displays them, which is annoying for about an hour and then becomes the thing that saves you, because those are the names that are actually stable.
Backups. Available on paid plans, and useful as a point-in-time safety net rather than as a migration tool. Take one before you touch anything. Take another before cutover.
Use the Data API for the migration and keep a CSV of each type alongside it. The CSV is not for importing. It is for arguing with, when the API returns something surprising and you need to know whether the surprise is in the data or in your code.
Turn the Data API on without opening the door
The Data API is off by default and enabled per data type in Settings, under API. Two things to get right before you enable anything.
The first: privacy rules still apply, and that is what you want. An export run with a token that is subject to your existing rules will silently skip records the token cannot see, and you will not notice until a count comes up short three weeks later. Run your export with an admin-level key, then check the counts against the editor before you trust a single row.
The second: turn it off again afterwards. An enabled Data API endpoint on a data type you forgot about is a permanent open question about your app. This is the most common thing we find that nobody meant to leave on.
before you export anything
- Take a backup, and check it exists rather than assuming
- List every data type and the record count the editor shows for each
- Enable the Data API only on the types you are exporting
- Export with an admin key so privacy rules cannot silently truncate the result
- Compare row counts against the editor after every run, per type
- Disable the endpoints you enabled
The count comparison is the whole quality gate. Do it per data type, not in aggregate, because two errors in opposite directions will cancel out in a total and both will still be wrong.
The four shapes that will not survive the trip
This is the actual work, and it is why "just export it" undersells the job.
Unique ids are Bubble's, not yours. Every record has a Bubble-generated id, and every relationship in your data points at one. You need those ids to survive the import so relationships can be rebuilt, and then you almost certainly want your own primary keys afterwards. Keep the Bubble id as a column during the migration. It is your only way to answer "is this the same record" when something looks wrong at three in the afternoon on cutover day.
Lists of things. A Bubble field can hold a list of other records. Postgres cannot, and should not: that is a join table. Every list field is a small schema decision, and there are usually more of them than anyone remembers. Find them first, because they change the shape of the import and therefore the order you have to run it in.
Option sets. These are neat in Bubble and they become either an enum or a lookup table, and which one depends on whether you expect the values to change. Get this wrong in the direction of an enum and every new value is a database migration. Get it wrong the other way and you have a two-row table with a foreign key. The second mistake is cheaper.
Empty, missing and never-set. Bubble is relaxed about the difference. Postgres is not, and neither is any type system you are about to introduce. A number field that has never been set, one set to zero, and one that was cleared can arrive looking similar and mean entirely different things. Decide per field, in writing, before the import runs.
Dates deserve a specific warning. Check what timezone your exported timestamps are in and what timezone your app was assuming, and check it against a record you can verify by hand. Doing that check takes ten minutes. Not doing it produces a class of bug that surfaces months later in a monthly report and is extremely difficult to trace back to this decision.
Files are a separate job
Uploads are not in your data export. They are URLs pointing at Bubble's file storage, and if you migrate the database without migrating the files you get an app that works perfectly until someone opens an attachment.
Two things to plan for. Volume, because downloading and re-uploading everything takes as long as it takes and you cannot compress that much. And permissions, because a file that was private in Bubble needs to be private in the new bucket, and the default when you move files in bulk is public. Broken links are embarrassing. A publicly readable folder of customer documents is a different category of problem.
Keep the old URLs in a column until you have verified the new ones resolve. Not until the migration script finishes, until you have actually opened some.
Do this even if you are staying
An export you have never run is a theory. The useful version of "we could leave if we needed to" is a folder on your own storage with a date on it and a row count you have checked.
Run it once a quarter. If it takes an afternoon the first time and twenty minutes after that, you have bought yourself a real answer to the question your next investor or acquirer is going to ask, and you have not committed to anything.
If you want the schema decisions above made properly rather than discovered during the import, that is most of what the audit is for. And if you are still deciding whether any of this is worth doing, the cost calculator will tell you to stay put when staying put is the right answer.