June 2026
All articlesBubble lets you store lists of custom data types very easily, but that abstraction does not map directly to a proper relational model in Postgres. That matters when you migrate.
In Bubble, any field can be a list, including custom data types. While this is a very convenient abstraction, its implications become much clearer once you need to migrate that data or model it in another platform.
First, it is important to understand that, in Bubble, when you create a field whose type is another Data Type, you are effectively creating a reference to another table. In a relational database, that is usually modeled with a foreign key, but Bubble abstracts that away by letting you simply choose which Data Type the field points to.
For example, if the orders table has a user field whose type is users, that means each order row points to a user row. In relational database terms, that is a foreign key from orders to users.
For example:
users
| id |
|---|
| user_a |
| user_b |
orders
| id | user |
|---|---|
| order_1 | user_a |
| order_2 | user_a |
| order_3 | user_b |
Here, the user field on orders points to a row in users.
In SQL, this could be translated to:
CREATE TABLE users (
id TEXT PRIMARY KEY
);
CREATE TABLE orders (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id)
);This part is simple. The problem starts when the field is not a single custom type, but a list of custom types.
Let us imagine we have the tables users and orders. Something you can do in Bubble is create a field on users that is a list of orders. This is a list of custom data types.
| id | orders |
|---|---|
| user_a | [order_1, order_2] |
| user_b | [order_3] |
In Postgres, there is no direct relational equivalent of “a column that stores a list of foreign keys” in the same clean way Bubble exposes it.
What this means is that there is not a direct one-to-one correspondence between a Bubble list of custom data types and a proper Postgres relationship.
The most naive solution would be to simply build a list of texts with the IDs that are referenced.
Following our previous example:
| id | orders |
|---|---|
| user_a | [order_1, order_2] |
| user_b | [order_3] |
The results we would obtain through the Data API for the users table would contain something like:
{
"response": {
"results": [
{
"id": "user_a",
"orders": [
"order_1",
"order_2"
],
...
},
{
"id": "user_b",
"orders": [
"order_3"
],
...
}
]
},
...
}This means the most direct migration would be to create the orders column in the users table as a list of texts. Here, we are treating Bubble unique IDs as text, not UUIDs.
CREATE TABLE users (
id TEXT PRIMARY KEY,
orders TEXT[]
);And that would store the values like this:
INSERT INTO users (id, orders)
VALUES
('user_a', ARRAY['order_1', 'order_2']),
('user_b', ARRAY['order_3']);While this is the most direct equivalent, it is not a very good long-term approach, because it is no longer a real relational structure. It makes joins harder, constraints weaker, and the whole model less sustainable.
The better solution is what Bubble most likely abstracts away for you under the hood: a join table.
Following the same example, instead of storing a list of order IDs inside the users table, you create a join table:
CREATE TABLE users (
id TEXT PRIMARY KEY
);
CREATE TABLE orders (
id TEXT PRIMARY KEY
);
CREATE TABLE users_orders (
user_id TEXT NOT NULL REFERENCES users(id),
order_id TEXT NOT NULL REFERENCES orders(id),
PRIMARY KEY (user_id, order_id)
);And the relationship would be stored like this:
INSERT INTO users_orders (user_id, order_id)
VALUES
('user_a', 'order_1'),
('user_a', 'order_2'),
('user_b', 'order_3');This is a proper relational structure.
It preserves the relationship explicitly, makes joins easier, and allows the database to enforce consistency properly.
That same idea applies to any Bubble field of type list.custom.*.
So the practical rule is:
TEXT[]The second approach is usually the better one if the relationship matters in queries, filters, joins, or downstream logic.
Additionally, you can keep the old columns temporarily as legacy fields for backward compatibility while you migrate to this relational structure.
Self-serve tools or a done-for-you service. Your app stays live while we move everything.