For approvers
Approving a deploy
Where requests reach you, what is worth looking at before you decide, and the one rule that has no exceptions.
Where requests reach you
All of these land in the same place and run through the same checks — the rule about who may decide, the role check and the expiry are enforced once, wherever you happen to click:
- The console. The Approvals screen holds the pending queue and the whole history. Each request opens as a screen of its own with an address you can paste into a ticket.
- A Slack DM or an email, on whichever channels your organization has turned on.
- The Bubble editor. If you open the branch a request was raised from, the panel over it lists what is waiting and lets you decide there.
- Your own tooling. BuildGate is a remote MCP server, so an assistant or a terminal can list what is waiting on you, read the change and decide it — see working from an MCP client.
- A pull request, where an administrator has switched that on for the app — see Git.
Note what is never a way to decide: a button in Slack, or a link in an email that approves on click. Both authenticate as whoever controls that account rather than as you, so every message links to the request in the console, where the decision is made under a Google sign-in and recorded against your name. Following one while a different organization is selected switches the console to the one the request belongs to.
What to look at before you decide
The request is laid out the way a pull request is:
- The change and the two versions it moves between —
source → target, as recorded when the request was raised. - The linked Linear issue, the change description and the rollback plan.
- The automated review, where one ran. Each finding reads as a heading with the reviewer's own line of context under it, so you can tell what was raised without opening the reviewer, and is marked as one that would have held the deploy or one it could have waved through. The review names which agent ran and what that agent could see. A review still running says so; one that errored is never shown as "nothing found"; one that could not see the whole change is never drawn as clean.
- What actually changed, where an administrator has switched on Show approvers what a deploy changes: which pages, workflows, data types, option sets and API calls were added, removed or changed, with privacy-rule, API and plugin changes called out, plus a pull request holding the full diff.
- The timeline, which is the audit chain filtered to this one request — the same events an auditor exports, not a friendlier copy kept alongside.
- The checks, under the timeline, the way a pull request lists its status checks above the merge button: the required approvals and separation of duties on every request, and Review first, Clean issue check and the deploy window where a ruleset turns them on. Each check is passed, pending, failed or skipped, with the ruleset that requires it and the reason when it is not passing. The list folds away once everything has passed. The last line says whether the request can go out now — waiting for approval, ready to deploy, deployed, or rejected. The service works out the checks from the same rules the Approve and Deploy steps enforce, so a check never reads as passed while either step would refuse.
You can also just ask. Anyone who can open a request can comment on it, including after it has been decided, and comments are chain events like everything else: they cannot be edited or deleted and they export with the rest. Your question and the answer are part of how the change was reviewed — in a Slack thread, they are not.
To take one request somewhere else — a ticket, a change advisory pack, an auditor's sample — Export PDF beside its title downloads its details, the automated review and the whole conversation as one file, worded exactly as the screen words them. Anyone who can open the request can export it. Each entry in it carries its sequence number in the chain, so it can be checked against the JSON export; the PDF is a rendering of the evidence, and where the two disagree the chain is the record.
Approving, rejecting, or handing it on
One box, one piece of text: what you write is recorded either as a comment on its own or as the note on your decision.
- Approving opens the gate for that one deploy, for fifteen minutes. It does not deploy anything — somebody still has to press Deploy: the person who raised it, or anyone else in the organization who is not an auditor.
- Approved requests from the same branch go out together. Bubble deploys the branch as it stands, so deploying one approved request also ships every other approved request raised from that branch to the same target. Each of them is marked deployed, says which deploy carried it, and has its issue updated the same way.
- Rejecting cannot be undone. The note is the feedback the requester reads, so say what would need to change. If you only have a question, comment instead and leave it pending.
- Re-routing changes who the request is routed to — one person or several — with a note saying why. Only the people newly named are notified. It grants nobody anything — any approver could always have decided it — and a request can never be routed to the person who raised it.
Who can decide, and who cannot
The request names an approver, and that is routing rather than
permission. Anyone in the organization with an approving role —
owner, admin or approver — can decide any
request they did not raise, from any of the places above.
One rule has no exceptions: whoever raised a deploy can never decide it, whatever role they hold. That is what the independent approval means, and it is enforced in the service, at every route, and by a constraint in the database. Requests you raised do not appear in your pending list at all.
Requiring the one nominated person instead would add no independence — a colleague on a flight is not a control — and it is how an approval queue quietly stops moving.
A request can name more than one approver — up to ten. Every person named is notified at once, and any of them may pick it up. How many approvals complete the request is still the number the app's rulesets ask for, never the number of names: route a request to two people under the default of one approval and the first of them to approve completes it; route it to one person where a ruleset asks for two and it still needs a second approval from somebody.
Roles
Held per organization, not per account: the same person can be an owner of one and a deployer in another.
| Role | Can |
|---|---|
owner | Everything, including transferring ownership. Cannot be removed by an admin |
admin | Manage apps, members and integrations; approve deploys |
approver | Approve a deploy someone else raised |
deployer | Request a deploy |
auditor | View only: read every deploy request, the exception queue and the evidence exports. Cannot raise, decide, comment on or resolve anything |
Roles also decide how much of the history somebody reads. Anyone who can approve sees
every request the organization has raised — that is what makes the Approvals screen a
record rather than a personal inbox. A deployer sees the ones they
raised.
An auditor is for whoever checks the control rather than operates it —
an external SOC 2 auditor, or your own compliance team. They read everything an
approver reads, including the exception queue, the deployments CSV, the event
export and chain verification, and every attempt to change something is refused,
whether it comes from the console, the extension, the API or an agent over MCP.
What the extension observes in Bubble is still recorded if an auditor happens to have
the editor open: that is a fact about what happened, not an action of theirs.
Every name on a request is a link: whoever raised it, whoever it is routed to, whoever decided it, everybody in the timeline. Following one opens that colleague's profile — the role they hold here, the Git account an approval on a pull request would be signed with, what they have raised, approved and rejected, and a grid of the last year with a square for each day, shaded by how much they did on it, so you can tell a colleague who decides deploys most weeks from a name on old records. Only people in your organization can open one, and only about somebody who is or was in it; anyone else gets the same answer as a name that does not exist. Somebody who has left keeps their profile, marked as no longer a member, so the requests they raised still lead to a person.
Rulesets
How a deploy of an app gets approved — and what happens once it ships — is set by rulesets, under Settings → Rulesets. A ruleset has a name, an enforcement status, the apps it targets, the rules it switches on and, optionally, a list of people and teams who may bypass it. Only owners and administrators can create or change one, and every change is written to the audit chain.
- Enforcement status. An Active ruleset applies to its apps; a Disabled one is kept as written and applies to nothing until it is made active again.
- Target apps. Pick apps one by one, or target all apps, which includes any added later.
- Rules. Required approvals, Auto-approve (and its low-risk widening), Review first, Approve in Git, Mark issue done, Check changes against issues, Gate merges into main, and two pre-deploy rules, Require checks to pass (whose first check is the Clean issue check) and Deploy window — each described below. Where several active rulesets target one app, their rules add up: a rule is on if any of them turns it on, and the highest Required approvals among them applies. The one exception is the low-risk widening, which only widens the auto-approval of its own ruleset.
- Bypass list. People listed are released from the ruleset's restrictions: they may approve before the review finishes under Review first, approve through a required check that has not passed, such as a failed Clean issue check, complete an approval before the rest of the Required approvals are in, and deploy outside the Deploy window. They are released only when every active ruleset imposing a restriction lists them, and every such approval or deploy is recorded as a bypass on the request. No bypass ever lets anybody approve a deploy they raised.
- Teams on the bypass list. A team, set up under Team → Teams, is a named group of members — the on-call rotation, say. Listing a team releases whoever is on it at the time, so changing who is on call means changing the team once rather than every ruleset. Someone released by one ruleset through a team and by another by name counts as listed on both. Deleting a team takes it off every bypass list, and removing someone from the organization takes them off every team.
An app's own settings dialog shows the rules it ends up with and which rulesets they come from, and warns when a rule cannot work on that app — Review first with no reviewer connected, or Clean issue check with no Bubble account connected, for example.
Automated approval, and what it is not
An app can be set to approve its own deploys when its review agent finds nothing wrong. It is a ruleset rule, off by default, and it is the one setting in this product that removes the second person from the control — so it is worth understanding before turning it on.
What counts as clean: the review completed without error, it saw the whole change, and every open finding is a low-or-no-priority comment whose title does not read as a security concern. Anything asked to be changed or tested, anything above low priority, and anything security-shaped holds the deploy for a person. A run that errored is never clean, and neither is one that read no comparison or a comparison cut short — could not look must never read as looked, and it was fine.
What it cannot do:
- It cannot reject, and it cannot deploy. A review that finds something, fails, or never runs leaves the request pending for its human approver, exactly where it was.
- It cannot outrun you. The assigned approver is notified as usual and can decide at any time; whoever gets there first wins, and a human decision is never overwritten.
- It cannot pretend to be a person. The decision is recorded as automated, never as compliant, and it names which agent gave it. Bubble's own deploy description says there was no human approver.
SOC2 CC8.1 expects an independent approval. For any app where this is on, be ready to justify it — and know which agent the app is set to, because a clean review from one of them is a statement about scope rather than correctness. See the two review agents.
Making approvers wait for the review
By default a person can approve a deploy the moment it is raised, and the review agent reads the change afterwards as a second opinion. An app can instead be set to require its review to finish before anybody approves. It is a ruleset rule called Review first, and it is off by default.
With it on, the review starts as soon as a request is raised, and Approve stays unavailable until that review has finished — in the console, from Slack, over MCP and from an approved pull request alike, because they all record a decision the same way. Rejecting is never held: saying no without waiting cannot ship anything. The review decides nothing itself; it is there to be read by whoever decides.
- Finished means stopped, either way. A review that errored or read only part of the change has finished too — the approver sees exactly that, and it can be run again.
- It fails closed. With no review agent connected, nothing for the app can be approved until one is, or until an administrator switches this off — except by somebody on the ruleset's bypass list. The app's settings warn when that is the case.
- A review that never started can be asked for. If the reviewer was out of credits or disconnected when the request was raised, the request's review tab offers to start one.
- It combines with automated approval. An app with both on runs one review: a clean one approves on its own, and anything else leaves the request with a person who can now decide.
Requiring a clean review
Review first makes approvers wait for the review to finish, whatever it found. An app can go further and hold the approval until the review passes: in the ruleset editor, turn on Require checks to pass and tick Clean automated review. It is off by default, and stays off when the other checks are switched on together, since it runs the reviewer on every request.
A review passes when it read the whole change and raised nothing a person needs to see — the same rule automated approval approves on, including the low-risk widening where a ruleset turns it on. The review starts when the request is raised and appears on the request's list of checks with the other checks, with its history.
- A finding holds the approval. The check names what the review raised. Fix it and raise a new request, or have somebody on the bypass list approve it, which the request records.
- It cannot be re-run until it passes. While the request waits for a decision, a review that read the whole change and said what it found is final. One that errored or saw only part of the change can be run again from the list of checks. Once the request is decided, expired or failed, any finished review can be run again from the Review tab as a second opinion, which changes nothing about the request; a deployed or withdrawn one cannot.
- It never approves anything itself. A person still approves, unless the app also lets a clean review approve on its own, in which case the one review does both.
- It fails closed. With no reviewer connected, or for BuildGate's own reviewer no comparison to read, the check cannot pass, and the app's settings warn about it.
- Rejecting is never held.
Requiring more than one approval
By default one approval decides a request: anybody with an approver role, other than the requester. A ruleset can ask for more with Required approvals — the number of approving reviews required before a request is approved, from 1 (the default) to 10. It cannot be set to zero: somebody other than the requester always decides.
- Each approval is counted as it is given. An approval that does not reach the number is recorded on the request, which stays pending and shows how many it has, for example 1 of 2. The approval that reaches the number approves the request, and the requester is told then.
- Different people. Approving twice counts once, and the requester never counts. Anybody who has already approved drops out of the approvals queue for that request.
- One rejection rejects it. Any approver, including one who already approved, can reject the request while it is pending.
- Every channel counts the same. The console, MCP clients and an approved comparison pull request each add one approval for the person who gave it.
- Holds apply to every approval. Review first and the Clean issue check are checked each time somebody approves, not only on the last approval.
- No automated approval. A clean automated review stands in for one approver, so where more than one approval is required it never approves on its own; the request waits for people.
- The highest number wins. Where several active rulesets target an app, the request needs the highest number any of them asks for. Changing a ruleset applies from the next approval on, including to requests already pending.
- The bypass list. Somebody on the bypass list of every ruleset asking for more than one approval can complete the approval with fewer, and it is recorded as a bypass on the request. Their own approval is still required: nobody is released from the one approval segregation of duties needs.
Every approval is in the audit chain — approval_added for one that left the
request waiting, approved for the one that completed it — and the evidence
export lists required_approvals and every approver in
approver_emails.
Gating merges into main
The deploy gate stands between an app's source branch and live. An app can also be
set so that merging another branch into its source branch needs an approved
merge request — promotion to main, reviewed the way a pull request into main
is, rather than inside the next deploy request alongside everything else merged since.
It is a ruleset rule called Gate merges into main, and
it is off by default. The source branch is whichever the app deploys
from: main, or test on Bubble's default.
A merge request is a deploy request with a different object. It names the branch being merged, an issue, a change description and a rollback plan; it goes to an approver; and every rule that holds a deploy's approval holds a merge's too — Review first, Clean issue check, automated approval where it is on. The requester can never approve their own, and the comparison an approver reads is the diff between the two branches.
How it works in the editor:
- On the source branch, merges are blocked. The extension refuses Bubble's merge call at the network layer, as it refuses an unapproved deploy, and opens the merge request form with the branch filled in. A small prompt in the corner of the editor opens the same form without trying first.
- An approval opens a window for one merge. The requester presses Open the merge window, then starts the merge from Bubble's own branch menu in the same tab. The window closes after one merge, or after a few minutes. Conflicts are resolved in Bubble as usual.
- Other merges are untouched. Merging between feature branches, and pulling the source branch into a feature branch, are never held.
- A merge that gets past the gate is a finding. One the extension sees reach Bubble with no window behind it, or one Bubble's change log shows completing with no approved merge request carried out before it, is an
unapproved_mergeexception naming who merged. - A merge is not a deploy. It is not counted in deploy totals or Insights, never matched to a deploy in Bubble's history, never moves an issue to done, and is not sent to Vanta as a change. The evidence export marks it with
kind = merge. - No break-glass, and the bypass list does not apply. Promoting work to main is not an emergency path; a hotfix goes out as a deploy. While the service is unreachable, merges into the source branch stay blocked.
- The deploy window does not apply. It is about production, and a merge into main is not a production change.
Like the deploy gate, this binds people using the extension. Somebody merging from a
browser without it is not stopped; they are found afterwards, from Bubble's change log,
the next time somebody with the extension opens the editor. Change-log checking covers
apps that deploy from test, the branch whose log the extension reads.
Holding a deploy on Bubble's issue checker
Bubble's editor keeps an issue checker — the counter in the corner listing broken expressions, missing fields and elements off the page — and a deploy with issues outstanding is the commonest avoidable production incident on the platform. An app can be set so that nobody can approve a deploy while that checker reports anything on the branch being deployed. It is a ruleset rule, and it is off by default: in the ruleset editor, turn on Require checks to pass and tick Clean issue check. It is the first of the checks that rule can require, and it works the way a required status check works on a pull request.
It runs through the Bubble account connected under Integrations, the same grant BuildGate's own reviewer reads the app with. The check is asked for twice: when the request is raised, so the requester hears about a broken branch straight away and the approver reads the answer on the request before deciding; and again on every attempt to approve, because the branch can change in between. Every run is written to the request's history, whatever it found.
- Clean means the whole app was checked and nothing came back. A check Bubble could not run over the whole app — some nodes skipped, a section that failed — holds the deploy too, because an empty list from a partial check proves nothing.
- It fails closed. With no Bubble account connected, or with one Bubble no longer accepts, nothing for the app can be approved until that is fixed or an administrator switches the rule off — except by somebody on the ruleset's bypass list, whose approval is recorded as a bypass and quotes what the check found. The app's settings warn when that is the case.
- Rejecting is never held. Saying no without checking cannot ship anything.
- It holds automated approval too, with no bypass. On an app that auto-approves clean reviews, a request the check does not clear stays with its human approver.
- It reads the branch being deployed — the app's source branch,
mainortest— not the live one, and it reads the branch as it is at that moment. - You can run it again. Fixed what it found? Press Re-run beside the check on the request, and the approver opens a request that says what the branch is like now. A re-run only adds to the history; every earlier run stays on the record, and none can be edited.
Holding a deploy that changes something sensitive
Some changes deserve a particular person's eyes whoever approves the rest: a privacy rule loosened, a data type added with nothing protecting it, a field removed that an integration still reads, a new backend workflow, a plugin. An app can be set so that a deploy changing any of those can only be approved by somebody on the ruleset's bypass list. In the ruleset editor, turn on Require checks to pass, tick Sensitive changes, and choose which areas count. It is off by default.
It reads the comparison BuildGate makes of the two branches, so it needs Show approvers what a deploy changes switched on. It runs as soon as that comparison arrives, so the approver reads the answer before deciding, and again on every attempt to approve.
- It names what changed. A failed check lists each sensitive area the deploy touches and what in it changed, on the request and in its history.
- It never passes without reading the whole change. With no comparison yet, a comparison that failed, an app too large to compare, or a comparison cut short before it reached every change, the check cannot answer, and that holds the approval too. The app's settings warn when comparisons are off.
- It says where to look, not that something is wrong. A change to a privacy rule may be exactly right. The check makes sure the people trusted with those areas are the ones who let it through, and the approval records that they did.
- Rejecting is never held, and an automated approval is held the same way, with no bypass.
Holding a deploy that carries an unsafe plugin
A plugin is third-party code running inside your app, and Bubble publishes nothing about which versions are unsafe. An app can be set so that a deploy whose branch has a plugin you have flagged can only be approved by somebody on the ruleset's bypass list. In the ruleset editor, turn on Require checks to pass, tick Plugin policy, and choose what it holds on. It is off by default, and is not ticked for you when you turn on the other checks.
- An open advisory. One your organization recorded on the Plugins screen, at the severity you choose or worse. A plugin whose version cannot be compared with the advisory's fix is held too: it may be affected.
- A plugin the ruleset does not allow. Picked from the plugins your apps have, or added by its marketplace id before anybody installs it.
- A plugin running its author's working copy. Unpublished code on its way to live. On unless you untick it, for a team that tests its own plugins that way.
It reads the branch's plugins from Bubble afresh — when the request is raised, so the requester hears early, and again on every attempt to approve — so an approval is never given on what the branch held yesterday. It uses your organization's Bubble connection, and while none is connected every request it covers holds.
Deploy windows and change freezes
An app can be set so that a deploy may only go out inside a schedule: weekly hours in a named time zone, less any freezes. It is a ruleset rule called Deploy window, and it is off by default. The schedule is written on the ruleset in the words a release manager uses — Mon–Fri 09:00–17:00, Europe/London, nothing from 24 December to 2 January — and read back to the requester in the same words.
What it holds is the deploy, never the approval. An approver can say yes on a Friday evening to a deploy that goes out on Monday morning; what the rule refuses is the moment somebody presses Deploy outside the window. The extension says why, names the ruleset, and says when the window next opens.
- Hours are read on the zone's own clock, daylight saving included. Hours that close at or before they open run past midnight, on the day they open.
- A freeze beats the schedule. It is a start, an end and a reason, and the reason is what the requester reads.
- No hours and no freezes means any time. The ruleset editor says so beside the switch rather than letting a rule that restricts nothing look like one that does.
- Several rulesets, several windows. A deploy has to fall inside every one that targets the app.
- The bypass list releases a requester from it, and a deploy that goes out that way carries the bypass on its token event — which rulesets, and what each one was closed for.
- It is preventive at the gate only. A break-glass deploy, or one made with the extension disabled, is not held by it; it shows up in the exception queue exactly as it would otherwise.
Closing the issue when a deploy ships
Every deploy request names the Linear issues or ClickUp tasks it delivers. An app can be set to move every one of them to Done once its deploy completes, so the tracker and the deploy record agree without anybody remembering to close it. It is a ruleset rule called Mark issue done, and it is off by default.
- Only a completed deploy moves the issue. A failed deploy, a rejected or withdrawn request and one that expired all leave it where it was.
- Done means the team's own completed state. A Linear issue moves to the first completed state in its team's workflow, whatever the team calls it. A ClickUp task moves to the first done status of its list, or to the list's closed status where it has no done status.
- A closed issue stays closed. An issue somebody already completed or cancelled — or a task already done or closed — is left as it is.
- It never holds up a deploy. The issue is moved after the deploy has been recorded, so the tracker being disconnected or unreachable costs the update and nothing else.
Checking changes against the issues
A request names the issues that authorised it, but nothing says the change is those issues. With the ruleset rule Check changes against issues on, every change listed in a BuildPrint review's technical changelog is matched against the request's Linear issues or ClickUp tasks — their descriptions, comments, checklists and attachment names — and the answer is listed on the request as Changes match approved issues. It is off by default.
- Every match quotes the issue. A model does the matching, and has to quote the issue's own words for each change it calls covered. The quote is looked for in what the tracker returned; one that is not there leaves the change uncovered. A model can miss a match, but it cannot make one up.
- BuildGate's own comments do not count. The comment left when a request is raised quotes the requester's description of the change, so any comment linking to a deploy request is left out of what the issue asks for.
- Advisory. The result is for approvers to weigh and never blocks an approval, but a change outside the issues is never auto-approved — nor is one the check could not confirm.
- The approvers are told, on the request. A change outside the issues gets a comment from BuildGate in the request's conversation, where whoever decides it is already reading. Nothing is posted to Linear or ClickUp. If the change was intended, tag the issue it belongs to, or add it to the issue and run the check again from the request.
- Tag the issue a change belongs to. Beside each change marked Not in scope, the person who raised the request and anyone who may approve it can Tag issue while it waits for a decision. Tagging one of the request's own issues leaves a note for the approver. Tagging an issue or task it does not name adds it to the request, posts the usual deploy-requested comment on that issue, and runs the check again, so the change only moves into scope if the new issue's own words ask for it. A tag never marks a change covered on its own, and an issue can only be added before anyone has approved. Every tag is recorded in the request's history under the name of whoever made it.
- Could not tell is not a pass. A changelog of more than 20 changes, a review that saw only part of the change, an issue that could not be read, or a model that did not answer all end as could not check.
- What it needs. BuildPrint as the app's reviewer, OpenRouter or Vercel AI Gateway for the model, and Linear or ClickUp.