What lives on the page is public
Anything Bubble sends to the browser should be treated as public.
That includes page data, visible text, hidden elements, reusable element properties, group variables, custom states, client-side API Connector calls, and the requests Bubble makes while the page runs.
Most users will not casually find it. Do not put anything on the page that depends on staying hidden from a technical user.
Secure the workflow action, not just the button
Hiding a button is good UI. Do not rely on it for destructive actions.
For example, in a marketplace app, users may be able to see listings they do not own. The delete action still needs to check ownership.
It is fine to hide the Delete listing button unless Current cell's Listing's Seller is Current User. Do that for the interface.
Then secure the workflow action as well. The delete step should only run when the listing belongs to the current user, or when the current user has the right admin role.
If the delete action has no permission check, the app is relying on the button staying hidden.
Not-clickable is also just UI
A disabled or not-clickable button stops normal users clicking something at the wrong time.
Use it for form validation, upload states, incomplete profiles, paid-feature prompts, and anything else where the interface should guide the user.
Do not use it as the permission check.
If the workflow changes data, check permission inside the workflow or on the action that changes the Thing. For important changes, I would rather see an explicit Only when on the action than trust that the button was not clickable.
Frontend workflow conditions can be bypassed
Frontend workflow conditions are fine for frontend behaviour.
Showing a popup, hiding a group, changing a custom state, showing a toast, or scrolling to a section can usually live on the page. If a technical user interferes with that, they have mostly changed their own browser.
Database changes need a stronger check.
If a workflow modifies an Account, Project, Listing, Team, Subscription, Invite, Role, or anything similar, check that the current user can modify that specific Thing.
For example:
Project's Owner is Current UserListing's Seller is Current UserCurrent User's Role is AdminTeam's Members contains Current User
The exact expression depends on the app. Put the check where the data changes.
Reusable properties and group variables are not permission checks
Reusable element properties and group variables are useful for keeping frontend logic maintainable.
They are a bad place to put the final permission decision.
For example, a reusable account component might have an Account property so the same reusable can be used by normal users and admins. That is a good pattern.
The save workflow still needs to check that the current user can modify that Account.
Otherwise, you are trusting that the reusable's Account property has not been pointed at a different Account the user can load. In practice, that kind of issue is easy to miss because the UI looks correct when you test it as the happy-path user.
Use reusable properties and group variables to decide what the component displays. Use workflow checks, backend workflows, and privacy rules to decide what the user can actually do.
Redirects do not protect data already loaded
Redirects are useful for page access. Use them.
A logged-out user should be redirected away from a dashboard. A non-admin user should be redirected away from an admin page. That is normal Bubble app structure.
The redirect must happen before protected data is useful on the page.
If the page loads private data and then a frontend workflow redirects the user, the data has already reached the browser. That is the issue.
For sensitive pages, combine redirects with privacy rules. The redirect keeps the user away from the page. The privacy rule stops the protected Thing or field being sent to the user in the first place.
Client-side API connector calls should not contain secrets
If an API Connector call runs from the browser, treat the request details as visible.
Do not put API keys, bearer tokens, signing secrets, admin credentials, internal IDs, or private sample values in client-side API Connector parameters.
Use private parameters, private authentication, shared headers, or a backend workflow. If the call needs a real secret, run it server-side.
One thing to flag: older forum posts about the API Connector mention initialised API responses being stored in client-side code. Bubble has changed parts of this over time, so I would verify the current behaviour before relying on an old post. I would still review existing API Connector calls that were initialised with real customer data, tokens, or admin responses.
The durable rule is simpler: if the browser does not need the secret, do not send it to the browser.
Privacy rules protect reads
If a user should not see a Thing or field, protect it with privacy rules.
Do not load all Projects into a repeating group and hide the rows the user should not see. The repeating group should only receive Projects the user is allowed to access.
Bubble is generally good at applying privacy rules to searches and actions. If a Thing cannot be found in searches, it should not appear as an empty row in a repeating group.
The leak can still come from the data model. If the user can search for Thing Y, and Thing Y stores a reference to protected Thing X, they may still learn the ID or relationship to X.
So check the whole path:
Can the user search for this Thing?
Can the user see the field?
Does another searchable Thing expose a reference to it?
Does a frontend workflow send it to the browser anyway?
Privacy rules are not a styling tool. They are where Bubble decides what data the user can receive.
Backend workflows should recheck sensitive changes
Backend workflows are a good place for private logic and sensitive mutations.
Use them when the workflow needs a secret, calls a privileged API, changes important records, or should not depend on frontend state.
For example, if the page lets an admin update another user's account, the backend workflow should receive the Account and then check that the current user is allowed to update it. Do not assume the frontend sent the right Account just because the reusable element was configured correctly.
Custom events can help keep repeated checks maintainable. That is the reason to use them. They are basically a reusable way to avoid copying the same permission expression into ten places.
But the sensitive action still needs to fail safely. If the user is not allowed to modify the Thing, the action should not run.
Quick check
For each sensitive feature, check this:
Is any private data loaded into a group, repeating group, custom state, group variable, or reusable property?
Does the workflow action check the Thing being modified?
Does the API Connector call expose a secret in the browser?
Do privacy rules stop the user receiving data they should not see?
If the answer is "we hide the button", fix it before shipping.