Detach a Supabase branch from its git branch without deleting it
A persistent Supabase branch linked to a git branch redeploys on every push to that git branch. Sometimes you want the deploys to stop but the branch - and its data - to stay: a staging environment you are freezing, or a branch whose automation you are handing to CI. Deleting and recreating the branch loses the data and the project ref. You do not have to. The git link is an editable field on the branch object, and clearing it stops the deploy loop. Everything below was measured on a GitHub-integrated project with a live persistent branch on 2026-08-21.
To follow along: a Supabase personal access token, the parent project ref,
and the branch ID (the UUID from GET /v1/projects/{ref}/branches, not the
branch name or project ref).
Step 1: Find the branch ID
Section titled “Step 1: Find the branch ID”REF=<parent-project-ref>curl -s "https://api.supabase.com/v1/projects/$REF/branches" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ | jq '.[] | {id, name, git_branch, persistent, status}'The git_branch field is the link. A branch created through the GitHub
integration has it set (e.g. "staging"); a branch created without a git
branch has it null.
Step 2: Clear the link
Section titled “Step 2: Clear the link”PATCH the branch with an empty string, not null:
BID=<branch-id>curl -s -X PATCH "https://api.supabase.com/v1/branches/$BID" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"git_branch":""}'The CLI does the same thing through the same endpoint:
supabase branches update <branch-name> --git-branch "" --project-ref $REFBoth paths measured working on a fully deployed (FUNCTIONS_DEPLOYED)
persistent branch. The change persists: a follow-up GET shows
"git_branch": "".
From a typed client or an IaC provider, send the empty string explicitly.
“Unset”, “remove attribute” or an optional field left empty serialise as
null or omit the key. null was measured to leave the link in place (next);
an omitted key was not sent, and the API’s treatment of null as
field-absent is why it is expected to behave the same.
Step 3: Verify the deploy loop is off
Section titled “Step 3: Verify the deploy loop is off”Push a commit to the git branch that used to drive the branch. Measured A/B on the same branch, same git branch, before and after clearing:
| Push with link set | Push with link cleared |
|---|---|
Supabase Preview check run goes in_progress | Check run completed with conclusion skipped |
Branch goes to CREATING_PROJECT (full redeploy) | Branch stays FUNCTIONS_DEPLOYED, untouched |
The check run still appears on the commit because the GitHub App is still installed on the repository - it just does nothing. If the skipped check itself is noise, the remaining lever is the integration’s repository settings, not the branch.
Reattach the link
Section titled “Reattach the link”Relinking is the same call with the branch name back:
curl -s -X PATCH "https://api.supabase.com/v1/branches/$BID" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"git_branch":"staging"}'The name is validated against the connected repository - a branch that does not exist on the repo is rejected at create and update time. After relinking, the next push redeploys the branch again (measured).
Adjacent finding: deleting a persistent branch
Section titled “Adjacent finding: deleting a persistent branch”DELETE /v1/branches/{id} on a persistent branch
returns 400 {"message":"Cannot delete persistent branch."}. Set it
ephemeral first, then delete:
curl -s -X PATCH "https://api.supabase.com/v1/branches/$BID" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"persistent":false}'curl -s -X DELETE "https://api.supabase.com/v1/branches/$BID" \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"What to do about it
Section titled “What to do about it”- In automation, assert the follow-up GET shows
"git_branch": ""and fail the job otherwise; a 200 on the PATCH is not confirmation. Rests on thenullrow below: 200 with the field echoed back asnull, GET unchanged. - From a typed client or IaC provider, send the empty string explicitly.
“Unset” or “remove attribute” semantics serialise as
nullor omit the key.nullwas measured to leave the link in place; an omitted key was not sent. Rests on the same row; the API treatsnullas field-absent, which is why an omitted key is expected to behave the same.
How it was checked
Section titled “How it was checked”| Claim | How it was checked |
|---|---|
{"git_branch":""} clears the link | PATCH + GET round-trip on a live persistent branch, twice |
null is a no-op | Same round-trip; GET showed the link unchanged |
| Cleared link stops deploys | Pushed a real commit before/after; check-run state and branch status compared |
| Relink restores deploys | Re-set the link, pushed again; branch redeployed |
CLI --git-branch "" works | supabase branches update + GET round-trip |
| Delete blocked on persistent branches | DELETE attempt, then persistent:false PATCH + DELETE success |
The official branching docs cover delete-and-recreate for rollbacks and re-seeding but do not document clearing the git link; the behavior above is measured, not documented.1
References
Section titled “References”-
Supabase, “Branching,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching ↩