Bubble.io Database: How the Data System Works (Complete Guide)
Bubble runs on PostgreSQL. Privacy rules are row-level security. Every search is a SQL SELECT. Understanding what is really happening under the hood is the key to building apps that are fast, secure, and scalable.
Understanding Bubble’s Database — What It Is and How It Works
Bubble runs on a PostgreSQL database behind the scenes. You interact with it through Bubble’s visual data editor, privacy rules, and the “Search for” expression. Understanding how the database actually works — what happens when you create a data type, run a search, or set a privacy rule — is the difference between building apps that are fast, secure, and scalable versus apps that are slow, leaky, and fragile.
Data Types, Fields, and Relationships Explained
Data Types = Tables
A Bubble data type is a database table. “Task” is a table. “Project” is a table. Every record you create (every task, every project) is a row in that table with a system-generated unique ID. You see them in the Data editor as rows.
Fields = Columns
Every field you add to a data type is a column in that table. Text fields store strings. Number fields store integers or decimals. Yes/No fields store booleans. Date fields store datetimes with timezone. Image fields store file URLs.
Relationship Fields = Foreign Keys
When a field’s type is another data type (e.g., Task’s “project” field of type Project), Bubble stores the referenced record’s unique ID. This is a foreign key relationship. Accessing Task’s project’s name triggers a JOIN query behind the scenes.
List Fields = Arrays
A field of type “list of [Type]” stores multiple values in a single field. Useful for tags, multiple categories, or many-to-many relationships. Lists are stored as arrays in PostgreSQL and searched with “contains” operators.
Privacy Rules = Row-Level Security
Bubble’s privacy rules translate to PostgreSQL row-level security policies. When you write a privacy rule “only show if created_by = Current User”, Bubble appends a WHERE clause to every SQL query on that table. This is server-side — data is filtered before it ever reaches the browser.
Search for = SELECT Query
Every “Search for [Type]” expression in Bubble generates a SQL SELECT query. Constraints become WHERE clauses. Sort becomes ORDER BY. Pagination becomes LIMIT and OFFSET. Understanding this mental model helps you write efficient searches that generate efficient queries.
How to Write Database Queries That Actually Perform
Search for Projects [
workspace = current_workspace, ← WHERE workspace_id = ‘xxx’
status = Active, ← AND status = ‘Active’
is_deleted = no ← AND is_deleted = false
]
// Database returns only matching rows — fast at any scale
// FILTERED search → loads ALL rows, filters in JavaScript
Search for Projects
:filtered by status = Active ← Loads EVERYTHING first
// Database returns all rows — slow and gets worse as data grows
Available Search Constraints and Their SQL Equivalents
| Bubble Constraint | SQL Equivalent | Field Types |
|---|---|---|
| = value | WHERE field = value | All types |
| is not value | WHERE field != value | All types |
| contains text | WHERE field ILIKE ’%text%’ | Text fields |
| doesn’t contain text | WHERE field NOT ILIKE ’%text%’ | Text fields |
| > / < / ≥ / ≤ value | WHERE field > value | Numbers, dates |
| is in list | WHERE field = ANY(array) | Option sets, relationships |
| contains list item | WHERE list_field @> ARRAY[value] | List fields |
| is empty / is not empty | WHERE field IS NULL / IS NOT NULL | All types |
Order Constraints for Maximum Performance
The most selective constraint should come first in your search. If workspace = current_workspace reduces results from 1,000,000 to 500 records, put it first. If status = Active reduces from 500 to 50, put it second. Bubble evaluates constraints in order and returns early when possible. The most selective filter first minimises the work the database must do.
Accessing Your Bubble Database From Outside Bubble
// Exposes REST endpoints for each data type
GET https://yourapp.bubbleapps.io/api/1.1/obj/task
Headers: Authorization: Bearer USER_API_TOKEN
// Returns paginated list of Task records visible to that user
// Respects privacy rules — only records the user can see are returned
GET https://yourapp.bubbleapps.io/api/1.1/obj/task/UNIQUE_ID
// Returns a single Task by ID
POST https://yourapp.bubbleapps.io/api/1.1/obj/task
Body: { “title”: “New task”, “is_done”: false }
// Creates a new Task, created_by = authenticated user
Ready to Build Your App on Bubble?
Architecture review, data model design, Stripe billing, and full builds — done right from day one by Pakistan’s leading Bubble.io team.