Data modelling decisions that scale
Model data around the searches and privacy rules
Start with the searches, privacy rules, and workflows the app needs.
If a Task belongs to a Project, the Task should probably have a Project field of type Project. Then your repeating group can search for Tasks where Project is the current Project, and your privacy rule can check the Task's Project.
Do not store projectId as text unless it is an external ID from another system. In Bubble, a field can hold the actual Thing. Bubble handles the unique ID behind the scenes. In the expression composer, you usually want Result of step 1, not Result of step 1's unique ID.
This matters because text IDs are a pain to work with. You lose Bubble's normal field chaining, searches get clunkier, and another developer has to remember that projectId is secretly pointing at a Project.
One exception is an external API. If Shopify gives you a variant ID, store that as text because it is Shopify's ID. If the field points at a Bubble Thing, use the Thing type.
Put the relationship on the side you search from
For most parent-child relationships, put the parent on the child.
A Message has a Conversation. A Task has a Project. An Invoice has an Account. This makes the normal Bubble search straightforward:
Do a search for Tasks where Project = Current page Project
It also helps privacy rules. If the Task has an Account or Project, the privacy rule can check that linked Thing rather than relying on a list somewhere else being correct.
Lists on the parent are useful when the list is small, ordered, or actually part of the parent record. For example, a Checklist might have a list of Checklist Items in a deliberate order. A Team might have a list of Admin Users if that list is used directly in privacy rules.
Be careful with large lists of children on parent Things. They are easy to forget to update when a child is created, moved, deleted, imported, or changed in a backend workflow. If the app can get the children with a normal search, that is often simpler.
There are cases where you store both directions. That is fine if you have a reason. Just treat the duplicated relationship as something you now have to maintain.
Use option sets for product constants
Use option sets for values the app defines.
Good option sets:
Order Status
File Type
User Role
Subscription Tier
Navigation Tab
Those values usually ship with the app and change rarely. They can have attributes like colour, icon, label, sort order, or which page they apply to.
Do not use option sets for records users need to manage. If admins need to create, rename, archive, reorder, or permission the values, use a data type.
Examples that should usually be data types:
User-created tags
Account-specific categories
Configurable CRM stages
Client-managed document types
A common bad pattern is creating several option sets for the same kind of thing. For example, separate Order Status, Shipment Status, and Return Status option sets may be unnecessary if they are all statuses.
You can often create one Status option set, then add an attribute like Status Types with values such as Order, Shipment, and Return. In the UI, filter the statuses by the current type.
Same idea for tabs. If every page has its own Tabs option set, ask whether Tab should be one option set with an attribute for which pages it belongs to.
Keep separate option sets when the concepts are genuinely different. Merge them when the difference is just an attribute of the same concept.
Do not use Creator for ownership
Avoid using Bubble's default Creator field as your ownership model.
Creator records the user context that created the Thing. It may not be the owner.
Backend workflows, webhooks, API Connector calls, app connector calls, imports, and admin tools can all create records. In those cases, Creator may be empty, an admin-like user, or simply the wrong person for your privacy rules.
Use explicit ownership fields:
UserAccountOrganisationTeamuploadedBycreatedFor
If a File belongs to an Organisation, add an Organisation field. If a Message belongs to a User, add a User field. If an admin creates something on behalf of a client, store the client or account it belongs to.
This makes privacy rules much easier to read. It also makes data migrations safer. When you later add accounts, teams, or workspaces, you are not trying to work out ownership from whoever happened to trigger the original workflow.
Creator can still be useful as audit information. I just would not build important permission logic on it.
Model nested files as one tree
Nested file structures are a good test of whether the model is actually usable.
You need to show files and folders together, stop unfinished uploads from hanging around, show breadcrumbs, and delete children when a folder is deleted.
I would usually use one File data type for both files and folders. Add a File Type option set with values like File and Folder.
Useful fields might be:
File TypeParent FileAccountorOrganisationuploadedByFile Statusfile URL or Bubble file field
name
Then a folder is just a File where File Type is Folder. A document is a File where File Type is File. Your repeating group can show both because it is displaying the same data type.
For navigation, the current folder can be a URL parameter. Breadcrumbs can also come from the URL path or from walking up Parent File, depending on the app. The important thing is that the File model supports the parent chain.
For uploads, create the File record early with a status like Pending. Schedule a backend workflow to delete it later if it is still Pending. When the user confirms the upload, change the status to Complete. That handles the annoying case where a user uploads a file, closes the popup, and never saves the form.
For deleting folders, use a backend workflow with clear stop conditions. It needs to find the children, then their children, and so on. This can get expensive if you accidentally schedule forever, so build the workflow carefully and test it on a small folder tree first.
One thing to flag: do not put ownership only on the top folder and assume every child is fine. Add the Account or Organisation to each File unless you have a very good reason not to. It makes searches and privacy rules simpler.
Use satellite data types only when they earn the complexity
A satellite data type can help when one field is heavy and most searches do not need it.
For example, a document chat app might have a File browser. The browser needs file names, folders, owners, and statuses. It does not need the extracted text of every document.
If the extracted text is stored directly on File, loading the file browser may load a lot of text the user cannot see. A separate File Content data type can keep the File light and load the extracted text only when the chat or search workflow needs it.
The cost is maintenance. If the File is deleted, the File Content needs deleting. If privacy rules change, both types need checking. If the extracted text is regenerated, the satellite Thing needs updating.
The useful test is the update-to-search ratio.
If the field is read far more often than it changes, a satellite data type may be worth it. If the field changes constantly, keeping another Thing in sync may cost more workload and developer time than it saves.
I would not create satellite types by default. Use them for heavy fields, repeated searches, or cases where loading the main Thing brings back too much data. Otherwise keep the data model boring.
Quick check before adding a field
Before adding a field or data type, ask:
Will this be searched?
Will this be used in privacy rules?
Who owns it?
What workflow creates it?
What workflow changes or deletes it?
Is this a Bubble Thing, an option, or an external ID?
If the answer is fuzzy, fix the model before building more UI on top of it.