20 tips to build maintainable Bubble apps

I run an agency that only works because we can build things maintainably (meaning that features are easy to revisit and iterate at later on). Here’s what I’ve learned from experience:

  • Almost all popups should be reusable elements.

  • URL parameters are generally much easier to work with than URL paths.

  • Every app needs a ‘Sign Up’ backend workflow that runs when a User is created.

  • CSS should be stored in an option set with the style and ID as attributes.

  • A single reusable popup can be used for all confirmations (including deletion) in an app.

  • Every app should have a Billing Account as a separate data type to store payment related data (e.g., stripe customer ID, subscription data). You’ll thank yourself when your B2C app starts allowing team members.

  • A global reusable element can be placed anywhere, and its custom events can be used to have reusable workflows across your app.

  • In the backend, instead of statically defining API tokens in URLs (e.g., ?api_token=XXXXX), have a custom event that returns an admin API token and use that.

  • Backend custom events are environment variables.

  • Front-end custom events help you modularise and make sense of madness.

  • API calls to your own app can use [Website Home URL]api/1.1/wf/your-workflow.

  • It’s rarely wise to store plan/product data in option sets. Store features in option sets, but limits, pricing, IDs, etc., should be in a data type.

  • Large workflows with many actions don’t make you look smart - modularise into smaller components.

  • Never launch without at least basic analytics.

  • Group variables are generally easier to locate and understand than custom states and can be dynamic.

  • Add privacy rules first, as soon as you create a data type.

  • If your data type’s fields have common prefixes (e.g., addressLine1, addressLine2, addressCity), then it strongly implies it should be a separate data type (Address).

  • Install plugins judiciously, and uninstall them as soon as possible when not being used.

  • Every API Connector JSON body parameter should not have quotes. All API Connector input expressions should have :format as JSON safe. That guarantees your calls are always JSON safe.

  • Loading states should almost always be a reusable element.

27 Likes

great points, i do half of these already but will implement others too! can you give more details about these? maybe specific use cases.

1 Like

URL paths are so fiddly because Bubble doesn’t have a very natural way of navigating with them. It doesn’t have a concept of a parent page/path. So, you end up with a lot of hardcoded links in your Go to page actions etc.

Every app is different, but most of our apps have the following options:

  • SPA View (something that appears in the sidebar)
  • SPA Tab (a tab that belongs under a view)
  • SPA Subtab (a subtab that lives under a tab / within it)

These are related (e.g an SPA Tab may have a List of Subtabs, and multiple tabs could belong to multiple views).

You will want to be able to sign up users from multiple places in your app e.g sign up page, admin panel, invite page.

All of those need the same logic, which might include:

  • creating a billing account
  • adding them to your email marketing platform
  • setting up certain fields
  • sending emails
  • sending analytics events

By consolidating that into one workflow, you can funnel all kinds of user creation operations through one set of logic so that you don’t repeat yourself (e.g have a parameter ‘isInvited’, and when ‘isInvited’ is yes, send the email which says they’ve been invited, and who by, etc.

Additionally, it’ll speed things up for your users. None of those need to happen immediately, they can wait a few seconds in most cases. So, offload them to the backend, while the user continues merrily towards paying for your product (hopefully).

2 Likes

thanks for the quick answers pretty good points again. do you have any suggestions for end user permissions, managing user roles (for an inventory system e.g.) but user can have different roles in different warehouses etc.

@jo1 : Here’s one brief explanation, along with a follow-up about privacy rules.

thanks @davidb it looks good for my need, i will give it a try

Thank you George for taking the time to post this.

Although where I’m at with my learning of Bubble right now I have no idea what some of this means, I’m sure in time I’ll come back to this and it will all make better sense.

3 Likes

Thanks for the tips!

Not totally true. Need to consider: number/integer, boolean and null value that doesn’t require double quotes and cannot be used with json-safe.

6 Likes

Thanks for all of your advice. I use to do so for many points but one time I do, one time I forget, and that the point… and some of your advise are new also for me… AND ONE POINT that I don’t understand !!! “”“Backend custom events are environment variables”, if you can explain please… thank you in any case

1 Like

@georgecollier, great content, as always :pray:

Can you elaborate on what you mean by loading states that should almost always be a reusable element?

I believe that the opposite is often the best strategy. For a complex app, privacy rules are going to get in the way of building the app and testing it.

This is assuming that the developer knows what he/she’s doing in Bubble and understands DB architecture well (ideally with experience outside of low-code tools). That way, when the developer plans the app, the data types will include the fields required for filtering and searching, as well as enforcing privacy policies. If that’s the case, there shouldn’t be a need to add the privacy rules as soon as a data type is created.

Obviously, if a developer isn’t 1000% certain he will remember to add privacy rules before launching, that’s a different story.

Nah, and that’s a hill I’ll die on. Debugging an app after adding privacy rules later is insane. No way to test every edge case.

If you understand your privacy rules effectively, you’ll almost never run into privacy rule related issues.

2 Likes

Simplify the database to make the privacy rules transparently correct.

Transparently secure database more valuable than opaque PrIvacy Rules. Says Yoda.

3 Likes

Agreed - mental scarring to prove it

2 Likes

That’s a separate batch of testing. Privacy Testing isn’t in the first round of testing if an app is doing complex stuff. You first gotta make sure the complex stuff is working properly. And since Bubble doesn’t let you create batch test data or test much from the backend (think server scripts or API connector data actually running within a API WF) it seems silly to me to try to debug something when it may just be the privacy rule in place that’s causing the issue. It’s a whole different mindset of testing privacy and testing functionality in a robust app, at least, and my brain can’t handle focusing on both at the same time.

1 Like

I think at the minimum you want to hide the data from the public, it doesn’t have to be role based privacy rules that are fleshed out.

2 Likes

Beat me to it. I have been using JSON safe but it doesn’t always work out properly. It does save a headache of trying to get between double quotes to add a dynamic expression, but there are types of data it doesn’t work with.

This is 100% true. Never would I ever add privacy rules first, makes no sense at all especially as we are able to add new fields, and of course when we forget to adjust the privacy rules for the new fields, we will end up with a headache trying to debug just to uncover it was the privacy rules.

Don’t make them of type popup, make it a group and put it into a popup on the page as the actual reusable element will be more versatile and capable of being a group on the page, or in a floating group, group focus or even a popup on the page itself.

Easier to work with, but not beneficial and can be detrimental to SEO. This needs to be understood for those who want SEO benefits from their URL structure.

I like the idea of the css as an option set, but the ID I wouldn’t do…I have 3 repeating groups on a single page, need separate IDs for each, but the same CSS on each, however, they do have some differences such as their height settings. But if a developer expects never to apply the same CSS to more than one element, than it could be okay to store the ID as an attribute, however, then there wouldn’t be a reason to keep it as option set since it is used only once, other than the true benefit of being able to use the app search tool to find where it is applied. I do this for similar settings and it is a life saver to have the app search tool make it possible to find all elements with id “Z” (Z is a better letter than X)

Caveat here is that the reusable needs to be placed into every area of the app within the editor so as to access them since if the reusable element with the custom events is not on the page or reusable from which you want to trigger the custom event, it is not possible to do…unless I’m missing something on this.

Is this for security purposes or flexibility in changing the token or having multiple tokens?

Used for backend workflows only or is there a way to use them for client side workflows as well?

Sometimes developers can create madness of custom events…but true, it does make it easier to modularize large sets of workflows

Depends on budget…the app editor data view can be used for some purposes, like seeing number of new users in a given time period. Sometimes it would make more sense from a budget standpoint to just export as CSV and use google sheets for chart display etc…as long as the analytics are just for internal use.

Also inputs can be used as part of calculations to derive values for other inputs such as for checkout summaries.

Not really, it just implies the naming should be amended so as to not have the prefixes…there some considerations to be made when making a decision to split off to a separate data type, the naming convention used I would say is not one of them.

Overall some good tips.

1 Like

This is correct, I should say all string inputs.

You can use the IDs as classes not IDs, using Classify, so that you can have multiple classes per element.

Primarily it means you don’t have to paste it every single time, and can change it easily, hence:

And you can duplicate those in front-end custom workflows if you want front-end environment variables.

Most analytics tools are free and can be installed in 5 minutes

Yes, really. In my example, Address should almost always be a different data type (it’s reasonable that multiple data types could have Addresses, or a Thing might have multiple addresses, etc. Sometimes you might have a location mirrored on its parent data type for searching, for example.

More examples:

  • phoneNumberHome, phoneNumberWork, phoneNumberMobile → Phone Number with number and type option
  • subscriptionStartDate, subscriptionEndDate, subscriptionStatus, subscriptionPlan → Subscription

etc

See Extract Class if you’re interested.

Thanks for correction on that…it is not good practice to use non unique IDs on an element but multiple classes on an element is fine. Should point out the difference between using IDs versus Classes.

Okay, I just first read the tip and assumed it meant a build out of analytics in the app…Yes, analytics like Google Analytics or Microsoft Clarity can be installed in the app in seconds copying and pasting the script; biggest effort is just setting up those accounts in the analytic provider dashboard

That has workload unit implications and doesn’t have as high a relevance in bubble database as it might in traditional databases. In Bubble I feel it basically boils down to a personal preference on structuring data and not so much as added functionality or reducing overheads. Below is a breakdown from AI (I don’t like formatting for clarity)

Should You “Extract Class” in Bubble?

:white_check_mark: YES, if:

  • The data is large and rarely accessed to avoid excessive data loading.
  • It removes redundant storage, such as repeated full addresses.
  • You need reusable relationships, such as a Company linked to multiple Users.

:x: NO, if:

  • The separation increases the number of database queries unnecessarily.
  • The data is always needed together, such as a User with Payment Info.
  • You are only doing it for a clean structure without performance or cost benefits.

In reality on this example, it just means instead of storing the components of an address as separate fields, you should structure the data to have a single field of type geographic address and always use that with the operator of extract to extract out the component that is needed, which often enough is just for display purposes and not functions as most functions would probably just use the single geographic address field.

Development time is generally much more expensive than workload units. It is rarely wise to sacrifice maintainability in order to save a few pennies of WU. And, maintainability is king, which is why this thread is about tips to build maintainably.

1 Like

Okay, so you increase development time by doing this though, although just the construction of the extra data type is not that much, nor is having the related data field on the main data type to keep the connection or vice versa, but the time to ensure that all data changes are kept in sync adds time, again not much, but overall there is more development time when needing to have separate data types that need to be in sync or called upon at the same time rather than just calling on the one type.

Have two separate data types is not more maintainable as you now need to maintain two different data types, one of which may have some text fields for the searching of the related type. If I need to find all things whose address is in a certain city and then filter those by distance from another address, then the two separate data types need a lot of doubling up of the data…so not really more maintainable.

Again, I think it really comes down to a personal preference on structuring the data and not so much as added functionality or reducing overheads…and definitely not reducing overheads, as development time is longer, maintainability is lowered (causes more time to maintain the separate data types).

At least in the example you provided of an address, it is not removing redundant storage as you then need to add the fields to the main data type for searching…

There are most definitely examples of when a separate data type is good, like an address for the company that all employees share, but the idea that the prefixed name is the reason to separate is not…that is just a preference for how to name things really.

And speaking of names, everybody should use Snake Case (name_is_this) and not Camel Case (nameIsThis), not Pascal Case (NameIsThis), nor any other type of naming convention for all data type name, field names and option display values. Snake Case em’ all. Why flexibility as you can easily find and replace the _ to craft camelCase or PascalCase if needed or into a normal view from booking_waiting_approval to Booking Waiting Approval

1 Like