Bootcamp
Lists, metadata and naming: the unglamorous parts of a good database
Photo by Yevgeniy Mironov on Unsplash
Nobody starts building an app because they’re excited about naming conventions. But ask anyone who has inherited someone else’s Bubble project what they wish the original builder had done, and the answers are always the same: name things clearly, don’t abuse list fields, use the built-in metadata, and write something down. These are the unglamorous parts of database work, and they’re what separates an app that’s a pleasure to maintain from one that’s a minefield.
This article covers the lot: how list fields actually behave (and three ways to build one), what Bubble’s metadata gives you for free, the naming rules our team uses across every app, a tour of Bubble’s data tabs, and the documentation that ties it together.
A list field is a single field on one row of data that holds multiple values — up to 10,000 of them, automatically deduplicated, stored in the order they were added. It’s a list on a row, not a list of rows, and confusing the two is the most common list mistake in Bubble.
How list fields really behave
Three ground rules first, because each one bites eventually:
- Lists hold at most 10,000 values. It’s a hard limit. Use list fields only for cases that will never reach it, or plan another strategy for when they do.
- Bubble deduplicates automatically. Add A, add B, add A again: the second A is silently dropped.
- Lists keep creation order. Enter C, then A, then B and the list stores C, A, B. You can apply
:sorted bywhen displaying, but the stored order is the order of entry.
A list can contain any basic type (texts, numbers, dates), custom data types (a list of Contacts), option sets (a list of tags on a contact), or API call responses.
The concept that trips almost everyone: one row contains the list. To reference a list field, your expression must first resolve to a single row. Search for Contacts:first item's Tags works: the search returns a list of contacts, first item narrows it to one, and only then can you read that row’s list of texts. You can’t pull the lists off all the contacts at once; you find your one thing, then reach into its list.
Lists also compare differently. A single field offers equals and is in; a list field offers contains, doesn't contain, is empty, isn't empty. And they’re populated differently: not text = a but text add a. Your options are add an item, remove an item, set the list (overwriting whatever’s there), add a whole list, remove a list, or clear it. The first two take a single item as input, the rest take lists. Sometimes you’ll reference the field circularly to modify it, as in Text set list = this Text:plus item ..., building the new list from the current one.
Finally, in workflows, keep the single/list distinction straight. Create a new thing, Make changes to a thing and Delete a thing operate on one row; Make changes to a list of things, Delete a list of things and Copy a list of things operate on many rows. To change a list field on one row, you want Make changes to a thing, not the list action, despite the name. Our honest advice for beginners is to lean on the single-thing actions and mostly avoid the list actions early on. The main use we have for Make changes to a list of things is actually just retrieving a list to use in later workflow steps, and for genuine bulk work we reach for scheduling an API workflow on a list.
Three ways to create and store a list
A quick, deliberately unpretty exercise makes all of this concrete. Create a page, add an Option data type with a name field and three rows (option one, two, three), then put a repeating group on the page with type of content Option and data source Do a search for Options, with a checkbox in each cell.
Way 1: a custom state, saved on demand.
- On the page, create a custom state called
selected options, type Option, ticking This state is a list. - Add a workflow on the checkbox’s value is changed event, with the condition this checkbox is checked: set the state to
selected options:plus item Current cell's Option. (If you ever need to put a single value into a list container and Bubble rejects the expression, append:converted to list, a handy trick.) - Duplicate the event with the condition isn’t checked and use
:minus iteminstead. Ticking and unticking now adds and removes items live. - To persist it, create a data type (say Option List Saved) with a field
chosen options, type Option, ticked as a list. A Save button then runsCreate a new thingwithchosen options set list= the custom state. The selection commits to the database in one row.
Way 2: the Multiselect Dropdown plugin. Install Bubble’s Multiselect Dropdown plugin, point its dynamic choices at Do a search for Options, and on save use the dropdown’s value instead of a custom state. Same result, none of the checkbox plumbing.
Way 3: a row per selection. Instead of a list field, create a data type OptionSelected with two fields: option (type Option) and a reference to its parent list record. When a checkbox is checked, Create a new OptionSelected; when unchecked, Delete a thing on Do a search for OptionSelected constrained to that option and that parent, :first item (delete works on one thing at a time).
The difference is when data commits. Ways 1 and 2 hold the selection in memory until the user presses save, which is right when nothing should persist until the end. Way 3 writes as the user goes, so if they stop and come back later, their selections are still there.
Metadata: the fields you get for free
Metadata is data about data, and Bubble creates it for every row: the created date, the modified date, the creator (type User), the unique ID, and a settable slug. The slug substitutes for the unique ID in URLs on pages with a type of content, which is friendlier for humans and for search engines; like the unique ID, it must be unique in its table.
Users carry extra built-ins: the password (which, for security, no one can ever retrieve, not even the user; it can only be reset), the email confirmed flag, which flips to yes when the user clicks the confirmation link and which you can use in conditions to gate features, and a Stripe ID if you use the Stripe plugin.
Metadata can also shape your data model. Say a deal needs an allocated date and a completed date. You could add two date fields. Or you could split out a Log data type: each log row links back to its deal, carries an event from an option set (allocated, completed, and whatever you think of later), and uses the built-in created date and creator as the when and the who. You get a better record of what actually happened, using fields Bubble maintains for you.
Naming conventions that hold up
Names carry the meaning of your data, so we treat them as critical. Our rules for data types:
- Name the type for what one row is. One row is an order? The type is called Order. One row is an FAQ? It’s FAQ. Direct, never abstract, and understandable to the next developer without consulting documentation.
- Singular, not plural. Bubble pluralises names in its database views, and a type called “orders” becomes “orderses”. Let Bubble handle the plurals.
- Reuse common names across apps. Ours include banner, FAQ, invitation, message, notification, log, process, product, resource, subscription and tag. Anyone on the team can open any of our apps and know what those tables contain.
And for fields:
- Common field names, used consistently:
name,displayanddescription(texts with distinct purposes);sort by(a number for ordering lists);status(an option set: draft, active, archived);type(an option set for rows of different kinds; one Company data type with buyer and seller types beats two near-identical data types);deleted(a yes/no flag field); androleon the user, referencing a user-role option set, which makes privacy rules straightforward to define. - Succinct, because long names get truncated in Bubble’s data tab.
- Don’t repeat the table name. On Order, the field is
total, notorder total. - No spaces or capitalised words in field names. It’s a stylistic preference over camel case or capitals, but pick one convention and stick to it.
- Unambiguous: the name should convey the field’s contents without needing an explanation, wherever possible.
One more habit: clean up after yourself, and do it as you go rather than at the end of the project. Modelling involves trying things, and that’s fine, but three leftover status fields on one data type means you’ll reference one somewhere and another somewhere else and thoroughly confuse yourself. Delete unused fields, data types, option sets and privacy rules. Leaving them is leaving little landmines for your colleagues to find and reverse-engineer.
Know your way around Bubble’s data tabs
Bubble’s Data section has five tabs, each with a distinct job:
- Data types: configure structure — the buckets, their fields, their relationships. This is where practical data modelling happens.
- Privacy: rules per data type controlling what different users (and logged-out visitors) can see.
- App data: the actual data, where you create, read, update and delete records from the editor. It also handles copy and restore, database rollback, migrating data between test and live (whole database or one type), CSV import and export, and bulk actions. When you build something, come back here and check the data says what you expected. Don’t assume.
- Option sets: create and manage sets, their attributes and their displays.
- File manager: uploaded files of any kind, referenced across your app.
Document the schema
Documentation gives you a framework for thinking that pays for itself: get the thoughts on paper, reorganise them there, then implement quickly. On complex features we’ve found that spending the overwhelming majority of the time on documentation lets the build itself go fast.
Our standard template covers the solution architecture, a sitemap, UML relationship diagrams, a database entity relationship diagram (the important one: every field, its type, key fields and the relationships marked), a condensed schema view, table definitions in plain language, notes on row types and statuses, the privacy-rules matrix of user types against data types, back-end workflows, risks, and the page architecture (URL parameters, custom states, groups, reusables). One hard-won tip: don’t build a single monster diagram. Split schemas and entity diagrams into functional areas (the user and what hangs off it, the contact, the deal) and read them in sections.
This skill in the AI era
Everything in this article is exactly what an AI coding agent needs from you. Clear entity names, consistent field conventions and a documented schema are context: hand them to Claude Code and it writes code that fits your system instead of inventing its own vocabulary; skip them and every generated feature names things differently. The discipline of cleaning up unused fields matters doubly when an agent can read your whole codebase and treat dead columns as live. This is a large part of how a Claude Code developer actually works day to day, and the habits carry over from Bubble almost unchanged.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.