Skip to content

Two Supabase projects on one GitHub repository

A monorepo holds two apps, apps/a/ and apps/b/, each with its own supabase/ directory and its own Supabase project. Both projects connect to the same GitHub repository through the Supabase GitHub integration, each with its own working directory, and both have Automatic branching on. This page covers which pull requests create a preview branch on which project, and whether a CI job can wait for one project’s preview when both report checks on the same commit.

Every figure marked measured comes from experiments/github-branching in the supabase-lab harness. Everything ran on 2026-09-23 on a Pro organization in ap-southeast-1, against two Micro projects and a private throwaway repository, all created and destroyed the same day. A second session from 03:27 UTC used two fresh projects and a fresh repository for the file-kind, late-change and merge questions; its modules (GB04-GB06), artifacts and RUNLOG Runs 5-7 are at 0c90777. A third session on 2026-09-29 (GB07, project A only) measured where a preview gets its secrets; its module, RUNLOG Run 8 and artifact are at 6f55c11. A fourth session on 2026-10-10 (BN01-BN03, two runs each, no git_branch) measured what a merge applies from a branch created through the Management API; it ran on a Pro organization in ap-southeast-1 and is described in the merge section. Project A was connected with Working directory apps/a, project B with apps/b. The branching docs the results are compared against were read the same day.12 Anything not marked measured is cited, or labelled as a design or as not run.

TL;DR:

  • A second project can connect to a repository another project already uses; B’s first attempt did not save and the reconnect did. Each connection keeps its own working directory.
  • With “Supabase changes only” off, each project created a preview for all 5 pull requests in the run, a root README.md change included. With it on, a project created a preview only for pull requests that changed files under its own <workdir>/supabase/. Migrations were tested on both projects; a seed.sql, config.toml, function or NOTES.md change under A’s <workdir>/supabase/ also triggered A.
  • Both projects posted check-runs named Supabase Preview, from the same GitHub App, to every pull request head commit read. Each project posts at least one skipped run; a project that does not branch posts 2, both skipped.
  • The docs’ wait-by-check-name workflow, path-filtered to app A, read project B’s run both times: B’s skipped on an A-only pull request, which skipped the migrate job, and B’s success on a pull request touching both apps.
  • The Management API branch list, which is per project, separated the outcomes: MIGRATIONS_FAILED for A and FUNCTIONS_DEPLOYED for B on the same pull request. The first sample, at 15 s, read FUNCTIONS_DEPLOYED on both, before CREATING_PROJECT appeared at 32 s.
  • With the setting on, a migration pushed to a pull request that opened without Supabase changes created no preview until the pull request was closed and reopened.
  • Merging a pull request that changed only app A started a production action run on both projects, with “Supabase changes only” on. One merge was tried.
  • On a branch created through the Management API with no git_branch, POST /merge applied what was written through POST /projects/{branch_ref}/database/migrations and nothing written through POST /projects/{branch_ref}/database/query, although GET /diff listed both (BN01-BN03, two runs each, 2026-10-10).
GitHub repositoryapps/a/supabase/apps/b/supabase/pull requesthead commitproject Aworking directory apps/aGitHub Appproject Bworking directory apps/bGitHub Apppreview branch Aif it branchescheck-runs on the commitall named Supabase Preview2 to 6 runspreview branch Bif it branches2 to 6 runs
  1. One pull request’s head commit reaches both projects through the one GitHub App.
  2. Each project decides on its own whether to create a preview branch.
  3. Both projects post their check-runs to the same commit under the same name, whether they branch or not.
GoalSetupStatus
A pull request that touches only app A creates only A’s preview”Supabase changes only” on, on both projectsmeasured
Every pull request gets a preview on both projects”Supabase changes only” offmeasured; 2 previews per pull request, each billed for its own usage3
A CI job waits for app A’s preview before seeding or testing itpoll project A’s branch list for the pull request’s git branchbranch-list signal measured; the wait loop below was not run in CI
A CI job waits on the Supabase Preview check by namedo not use with two projects connectedmeasured: it read the other project’s run
Only one project reports on a commitAutomatic branching off on one project; create its branches from CInot run: whether a connected project with Automatic branching off still posts check-runs is untested

Preview creation: the working directory and “Supabase changes only”

Section titled “Preview creation: the working directory and “Supabase changes only””

The docs define the working directory as “the path from your repository root to the directory that contains the supabase/ folder”, and the setting as “only create Supabase branches when Supabase files change”.1 Neither says which files count when two projects share a repository. GB01 opened the same five pull requests once with the setting off on both projects and once with it on.

Pull request changesOff: preview on AOff: preview on BOn: preview on AOn: preview on B
README.md at the repository rootyesyesnono
apps/a/README.mdyesyesnono
a migration under apps/a/supabase/yesyesyesno
a migration under apps/b/supabase/yesyesnoyes
a migration under eachyesyesyesyes

With the setting off, the working directory did not limit previews: each project previewed all 5 pull requests, 10 in all. With it on, each project previewed only the pull requests that changed a migration under its own <workdir>/supabase/ (5 shapes, one pull request each); apps/a/README.md is inside A’s working directory and did not trigger A. These two runs changed only migrations; a later run tested seed.sql, config.toml, a function and a non-CLI file.

Closing the pull requests removed every preview within 2 minutes in every run; nothing was left for the harness to delete.

The connection form set Branch limit to 3, and GB00 read limit=3 back on both projects. The run raised it to 10 on both before opening any pull request, so the limit could not cap the count.

In that later run, GB04, with the setting on, each pull request changed one file under apps/a/supabase/. All four kinds triggered a preview on A, NOTES.md included, though the CLI does not read it:

File changedPreview on APreview on B
seed.sql (a second insert)yesno
config.toml (a comment line appended)yesno
functions/probe/index.ts (a new function)yesno
NOTES.md (a file the CLI does not read)yesno

On the function pull request, A’s comment marked Edge Functions with a warning: “Only Functions declared in config.toml will be automatically deployed to branches”. Whether probe deployed to the preview was not checked.

With the setting on, a pull request opened with only apps/a/README.md got no preview on A. A migration pushed to it afterwards still produced none in 240 s, and closing and reopening the pull request produced one at the first 20 s poll. This was one pull request; late seed.sql or config.toml changes were not tried.

A seed.sql change pushed to a pull request whose preview already exists did not re-seed it: 240 s after the push the new row was absent from the preview database. The docs say the same: “The database is only seeded once, when the preview branch is created. To rerun seeding, delete the preview branch and recreate it by closing, and reopening your pull request.”2

With two projects connected, both projects’ check-runs land on the same commit.

On the head commitMeasured
Check-run nameSupabase Preview on every run, from both projects
GitHub App and check suitesupabase for every Supabase Preview run; one check suite on the setting-off commits (RUNLOG snapshot)
Runs from a project that branched5, once 6: 1 skipped, 1 success (or failure), 3 (once 4) in_progress when observation ended
Runs from a project that did not branch (“Supabase changes only” on)2, both skipped
Field that identifies the projectdetails_url only, of external_id, details_url, output title and output summary
external_idempty on every run
Output titleSupabase Preview or empty

Observation ran 600 s, or 480 s for GB03; the setting-off figures are a snapshot at 01:59:12 UTC, inside the window. Commits with the wait workflow also carried that workflow’s own github-actions runs. details_url links the parent project’s dashboard on the early runs and the preview branch’s own project on later ones.

GitHub’s check-runs endpoint defaults to filter=latest, “the most recent check runs”.4 In GB01 with the setting on, that view returned a single Supabase Preview run, and on 2 of the 5 pull requests, the A-only and the two-app one, the project that run belonged to changed during the build. The A-only pull request was sampled every 3 s for the first 150 s. In that window the name-filtered default view showed two different B skipped runs in turn, then A’s first in_progress, then A’s success. filter=all returns every run.

The integration also comments on each pull request, once per connected project, from supabase[bot]. Each comment starts with a [supa]:<ref> marker line. On a pull request that changed only apps/a/supabase/, B’s comment carried B’s own ref and read “This pull request has been ignored for the connected project … because there are no changes detected in apps/b/supabase directory.” A’s comment carried its preview branch’s ref instead of project A’s own ref, with its Deployments and Tasks tables and the line “Tasks are run on every commit but only new migration files are pushed. Close and reopen this PR if you want to apply changes from existing seed or migration files.”

On a one-app pull request the ignored project’s comment is the one carrying its own ref or the “has been ignored” text. A branching project’s comment carries its preview’s ref, and only the parent’s branch list (project_ref) maps that back to the parent. The early check-runs’ details_url carries the parent ref directly. Comments from two projects that both branched were not compared.

The branching docs show a CI job that waits for the preview with fountainhead/action-wait-for-check@v1.2.0 and checkName: Supabase Preview, then gates a migrate job on the result being success.2 The action lists check-runs by name with the endpoint’s default filter and returns the conclusion of the first completed one.5 GB02 added that workflow to the repository with a path filter of apps/a/supabase/**, the migrate job reduced to an echo, and “Supabase changes only” on for both projects.

Pull requestWait step returnedThat run belonged toMigrate jobA’s first success seen at
migration under apps/a onlyskippedproject Bskipped108 s
migration under each appsuccessproject Bran108 s

Times are seconds after the run started creating the pull requests, on a 30 s poll.

On the A-only pull request the wait step read B’s skipped run, started at 02:22:13 UTC. A’s own skipped run had started at 02:22:10 and already completed, so it was not the latest. A’s success run started at 02:22:58, and the poll first saw it completed at 108 s. The path filter only decides which pull requests run app A’s workflow; no B-only pull request was opened in this run. It cannot keep B’s runs off A’s commits: with “Supabase changes only” on, B posts skipped runs to every pull request commit it does not branch for.

The Management API’s branch list is per project. GB03 sampled GET /v1/projects/{ref}/branches on each parent project, matched on git_branch, on one pull request that broke A’s migration and left B’s valid:

First sample showing the value (s after opening, 15 s polling)A branch statusB branch status
15FUNCTIONS_DEPLOYEDFUNCTIONS_DEPLOYED
32CREATING_PROJECTCREATING_PROJECT
65RUNNING_MIGRATIONSRUNNING_MIGRATIONS
81MIGRATIONS_FAILEDFUNCTIONS_DEPLOYED

A loop that exits on FUNCTIONS_DEPLOYED can return at 15 s, before CREATING_PROJECT at 32 s and so before the preview project exists. The published OpenAPI document marks status as deprecated, with “List action runs to get branch status instead”.6 On A at 81 s, the preview project’s latest action run (GET /v1/projects/{preview_ref}/actions) showed migrate DEAD with deploy and seed still CREATED; on B every step was EXITED.

In GB02, 3 action runs carried a check_run_id pointing at that run’s commits, one per preview project: 1 on the A-only pull request and 2 on the two-app one. Each pointed at a check-run still in_progress at the end of the 600 s window, although every step of the action run was EXITED, so it cannot serve as a wait target. The parent projects’ action runs pointed at no check-run on those commits.

A wait loop on the branch list. The API calls are the ones the harness made; the loop itself was not run in CI.

Terminal window
# Needs SUPABASE_ACCESS_TOKEN and this app's parent project ref.
seen_busy=0
for i in $(seq 1 60); do
s=$(curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/branches" |
jq -r --arg b "$GITHUB_HEAD_REF" '.[] | select(.git_branch == $b) | .status')
case "$s" in
CREATING_PROJECT|RUNNING_MIGRATIONS|MIGRATIONS_PASSED) seen_busy=1 ;;
MIGRATIONS_FAILED|FUNCTIONS_FAILED) [ "$seen_busy" = 1 ] && exit 1 ;;
FUNCTIONS_DEPLOYED) [ "$seen_busy" = 1 ] && exit 0 ;;
esac
sleep 10
done
exit 1

MIGRATIONS_PASSED and FUNCTIONS_FAILED come from the OpenAPI enum and were not observed in the run.6 The seen_busy guard skips the FUNCTIONS_DEPLOYED reading at 15 s. It also means a loop that starts after the preview has finished never sees a busy state and times out. A job that can start late could compare the branch’s updated_at with the head commit’s time instead; the run did not read updated_at, so this is untested.

Both projects had Deploy to production on, with main as the production branch. The artifact does not record that value directly, but the merge module runs only if both default branch rows read git_branch main, and it ran. The dashboard form writes that field only when the option is enabled. The docs say that on a push or merge to it, new migrations are applied and the Edge Functions and Storage buckets declared in config.toml are deployed; they do not mention “Supabase changes only” or the working directory in that section.1 GB06 merged a pull request with one migration under apps/a/supabase/ and watched both projects for 240 s:

After the mergeProject AProject B
New production action runs11
Stepsclone, pull, health, migrate, seed, deploy EXITED; configure PAUSEDthe same
The merged migration in the project’s migrations listyesno
supabase app check-runs on the merge commit, at the end of the watch3: success + 2 in_progress3: success + 2 in_progress

In the one merge tried, B’s production run reached EXITED on the same six steps as A’s (clone, pull, health, migrate, seed, deploy) for a merge that changed nothing under apps/b/, with “Supabase changes only” on. Whether it changed anything on B was not measured, including whether deploy redeployed anything (B had no functions). seed reached EXITED on both projects, but whether it seeded anything was not checked; the docs say seed files are ignored on production deploys.1

A merge applies the migration history, not the schema difference

Section titled “A merge applies the migration history, not the schema difference”

A separate run on a branch created through the Management API with no git integration at all (medium-serverless MS15, 2026-09-30) found the first instance. POST /projects/{ref}/branches {branch_name, persistent: true} answered 201 and the branch read ACTIVE_HEALTHY 2 s later, supabase-postgres-17.6.1.171 at ci_micro. prisma db push (6.19.3) exited 0 in 14859 ms and left a table with a row on the branch, and GET /branches/{id}/diff answered 200 with 2016 characters of SQL - the pg_net extension, the sequence, the table, its primary key and index. POST /branches/{id}/merge then answered 201 {"workflow_run_id": "...", "message": "ok"}, and the parent had no such table 20 s later and still had no pg_net extension about 10 minutes later.

MS15 read this as: the merge is accepted and applies nothing when there are no migration files. BN01-BN03 (2026-10-10, two runs each) measured the mechanism behind that reading and narrow it, below.

The ACTIVE_HEALTHY reading is not a readiness signal for the parent’s schema. In BN01-BN03 each module created a throwaway parent in a Pro organization in ap-southeast-1 and one non-persistent branch off it, with with_data false and no git_branch. POST /projects/{ref}/branches answered 201 in 0.9 to 1.7 s, and the branch read ACTIVE_HEALTHY 1-2 s after the create call in all six runs. The parent’s baseline table was absent from the branch at that moment every time. In run 4 it was first seen at 53 s (BN01), 44 s (BN02) and 43 s (BN03). The branch workflow’s step list is not a completion signal either: BN03’s last read in run 4 had the baseline table present with migrate still CREATED. A GET /diff read inside that window, once by hand with the default and pgdelta=true variants, contained drop table "public"."bn_base" for the parent’s own table; the drop was gone once the table had arrived. Wait for the baseline table itself before diffing or writing.

Each module then wrote one change set on the branch: a table with an identity key, enable row level security, a select policy to authenticated, revoke all from anon and authenticated followed by grant select to authenticated, an immutable SQL function, and a column plus an index on a baseline table the parent holds. All writes were accepted in run 4 in all three modules (7 of 7), and the branch held the complete set. In run 3 BN01 wrote 5 of 7, because the probe started writing before the baseline table arrived and the column and index failed with 42P01; BN02 and BN03 wrote 7 of 7. The parent held none of the set before the merge in all six runs. The modules differ in the write route and the step before the merge:

ModuleWritten on the branch throughBefore the merge
BN01POST /projects/{branch_ref}/database/queryGET /diff in three variants
BN02the /database/query set and a second set through POST /projects/{branch_ref}/database/migrations, on one branch, one mergeGET /diff in three variants
BN03POST /projects/{branch_ref}/database/queryPATCH /branches/{ref} {request_review: true}, then GET /diff

What reached the parent after POST /branches/{ref}/merge, measured 2026-10-10:

SetIn GET /diffBranch migration history after the writeOn the parent after the merge
written through /database/query (BN01, BN03)every object, in the default, pgdelta=true and pgdelta=false variantsremote_schema only, unchangednone; polled 246 s (BN01 run 4), 245 s (BN01 run 3), 246 s (BN03 run 4), 258 s (BN03 run 3)
written through /database/migrations (BN02)every objectremote_schema, bn_m_changecomplete (table, RLS on, policy, SELECT only for authenticated, none for anon, function, column, index), first seen at the 42 s poll (run 4) and 56 s (run 3), with a bn_m_change row added to the parent’s history
written through /database/query, same branch and same merge as the row above (BN02)every objectremote_schema, bn_m_changeabsent when the poll ended, at 73 s (run 4) and 87 s (run 3)
MCP execute_sql, then apply_migration, then merge_branch, once by handnot readnot recordedexecute_sql set absent; apply_migration set present (table, policy, function), history row bn_y_change added

The merge answered 201 with {workflow_run_id, message: "ok"} in all six runs, and its workflow steps read clone, deploy, health, migrate, pull and seed EXITED and configure PAUSED at the end of the window. PATCH /branches/{ref} {request_review: true} answered 200 and set review_requested_at on the parent’s branch list, and the merge outcome was the one in the first row. The default GET /diff text was identical to ?pgdelta=false in all six runs, and ?pgdelta=true differed (1137 characters against 1430 for the default, BN01 run 4). The published OpenAPI document marks the endpoint Beta and describes pgdelta as “Use pg-delta instead of Migra for diffing when true”; it was read 2026-10-10.6 The pg_net extension appears in every diff variant of every run and was never installed on the parent after any merge. Reading of the six runs: the merge applied the rows of the branch’s migration history that the parent lacked, and ignored the rest of the difference that GET /diff listed. The platform’s merge implementation was not read.

The same by-hand trial found GET /projects/{branch_ref} answering 404 Project not found while POST /projects/{branch_ref}/database/query answered 201 on the branch ref (one trial).

So a merge is accepted and applies nothing when there is no migration history row to apply. A change written through the SQL route is in the diff and survives request_review, and the parent still lacks it. MS15’s 201 and nothing is the same outcome: after prisma db push the branch history held only the baseline row (inferred from the matching outcome; the Prisma path was not re-run in BN). For a db push workflow the promotion is prisma db push against the parent or prisma migrate diff generating supabase/migrations for the normal path. The Management API’s migrations route is a third path, and the one BN02 measured; whether supabase/migrations files reach a git-less branch’s history was not measured. The branch itself works as an isolated staging database: push, insert and diff all behaved, and the connection ran through the branch’s Supavisor tenant because db.<ref> is IPv6-only.

Not measured: the dashboard’s own SQL Editor and Table Editor routes, which reject a PAT, so whether Studio records a migration row for a change made there is unknown; the dashboard merge-request screen and its review diff; migration_version on the merge body; persistent branches in BN (MS15’s was persistent), with_data: true, edge functions, merging twice, a branch whose history conflicts with the parent’s, and a parent that already has migration files. Two earlier harness attempts of BN01 were stopped and left no artifact; one over-ran its background-task limit and one was killed by hand while the call wrapper retried a 200 body it had flagged as throttled.

Two harness notes from the MS15 run: a create call can time out at a client default of 30 s while the platform creates the branch anyway, so adopt the branch on the retry rather than creating a second; and a persistent branch refuses deletion with 422 Cannot delete persistent branch. until PATCH {persistent: false} answers 200.

The branching configuration docs, read 2026-09-29, say in their persistent-branch section that secrets set for one branch “are not automatically available in other branches”. They also say config.toml reads secrets through env(NAME), and that for a committed supabase/.env.preview encrypted with dotenvx the branching executor “will automatically retrieve and use these values”, once the private key from .env.keys is set with supabase secrets set.7 They do not say what a preview gets when an env() reference has no value anywhere, or when .env.preview was encrypted to a key the project does not hold. GB07 opened four pull requests under apps/a/supabase/, each adding a probe function (secretprobe) that reports, per secret name, whether the value is present and its SHA-256, so no value was recorded:

Pull requestBranch statusAction run stepsProbe functionSecret on the preview
PVLAB_GB07_PARENT set on parent A only, not referenced in config.tomlFUNCTIONS_DEPLOYEDall EXITEDHTTP 200absent from the preview’s secrets list and from the function’s environment
dotenvx .env.preview holding PVLAB_GB07_DOTENV, private key set on parent A as DOTENV_PRIVATE_KEY_PREVIEW, mapped through [edge_runtime.secrets] and env()FUNCTIONS_DEPLOYEDall EXITEDHTTP 200present; the function saw the value GB07 set, and the secrets list carried its SHA-256
A .env.preview built the same way, holding PVLAB_GB07_WRONGKEY, encrypted to a second keypair whose private key was discarded (parent A never held it)MIGRATIONS_FAILEDclone DEAD, every other step CREATEDHTTP 404, nothing deployednone: the run stopped before any deploy, and the preview’s secrets list came back empty
env(PVLAB_GB07_UNSET) in config.toml, with no value anywhereFUNCTIONS_DEPLOYEDall EXITEDHTTP 200absent from the preview’s secrets list and from the function’s environment

GET /v1/projects/{ref}/secrets listed value as the SHA-256 of the value set for both secrets GB07 could check: PVLAB_GB07_PARENT on the parent, read before any preview, and PVLAB_GB07_DOTENV on the dotenvx preview. DOTENV_PRIVATE_KEY_PREVIEW itself was absent from the three previews that returned readings (inherit, dotenvx, unset), so the dotenvx value was decrypted with the parent’s copy of the key. That is inferred from the key’s absence on the dotenvx preview and the wrongkey failure; no pull request ran with the key missing from the parent as well.

The wrong key stopped the run. The integration’s pull request comment, read by hand after the run and not recorded in the artifact, carried the cause: failed to parse config: ... 'edge_runtime.secrets[pvlab_gb07_wrongkey]' failed to decrypt secret: cannot decrypt ciphertext: cipher: message authentication failed. The branch status read MIGRATIONS_FAILED, although no migration ran, so a CI job waiting on status sees a failure but not its cause. The unset env() reference produced a function deployed without the secret: neither the branch list (FUNCTIONS_DEPLOYED) nor the action run (every step EXITED) reported it. That pull request’s check-runs and bot comment were not read.

PracticeEvidenceModule
Turn “Supabase changes only” on for every project sharing the repository.Off, each project previewed all 5 pull requests; on, only those changing its own <workdir>/supabase/.GB01
Keep files that should not trigger a preview outside <workdir>/supabase/.apps/a/README.md inside A’s working directory did not trigger A with the setting on.GB01
Raise Branch limit before relying on per-pull-request previews.GB00 read limit=3 back on both projects as connected.GB00
Do not wait on the Supabase Preview check by name with two projects connected.The docs’ workflow for app A read B’s run on 2 of 2 pull requests.GB02
Read check-runs with filter=all when inspecting a shared commit.The default view returned one run per name, where a commit on which both projects branched carried 10 or 11 Supabase Preview runs: 10 on the five setting-off commits (RUNLOG snapshot) and 11 on the setting-on two-app commit.GB01
Wait on your project’s branch list, matched on git_branch, once a non-terminal status is seen.status separated MIGRATIONS_FAILED from FUNCTIONS_DEPLOYED per project, after a first reading of FUNCTIONS_DEPLOYED at 15 s.GB03
Do not wait on an action run’s check_run_id.It pointed at a check-run left in_progress after the action’s steps had finished.GB02
Open a pull request with its Supabase changes, or close and reopen it after adding them.A late migration created no preview in 240 s; close and reopen created one at the first 20 s poll.GB05
Close and reopen the pull request to re-seed a preview after changing seed.sql.The new row was absent 240 s after the push; close and reopen is the docs’ remedy2 and was not run.GB05
Expect a merge that changes one app to start a production run on the other project too.An A-only merge started a production action run on B and put 3 of B’s check-runs on the merge commit.GB06
Identify the ignored project’s bot comment by its [supa]: ref or “has been ignored” text.B’s comment carried B’s own ref and named apps/b/supabase.GB04
Map a branching project’s comment to its parent through the branch list’s project_ref.A’s comment carried its preview’s ref, on one-app pull requests only.GB04
Set each secret a preview needs on it or in an encrypted .env.preview.Do not expect the parent’s: PVLAB_GB07_PARENT on parent A, not referenced through env(), was absent from the inherit preview’s secrets list and its function’s environment; the dotenvx value reached the preview. Setting a secret on the preview itself was not run.GB07
Keep DOTENV_PRIVATE_KEY_PREVIEW on the parent; re-set it on each re-key.With the dotenvx private key on parent A, the dotenvx preview’s function saw the value (the key’s role is inferred, see Evidence); a file encrypted to a key parent A never held died at clone. Re-encryption itself was not run.GB07
Read the bot comment, not only the branch status, when a preview fails.A decryption failure at clone read MIGRATIONS_FAILED (GB07); the wrongkey pull request’s bot comment, read by hand and not in the artifact, named the secret and the error.GB07
In CI, check each secret a preview needs is on its secrets list by name.An env() reference with no value reached FUNCTIONS_DEPLOYED with every step EXITED and the secret absent.GB07
Compare secrets by digest: the secrets list returns SHA-256 values.Parent A listed PVLAB_GB07_PARENT, and the dotenvx preview listed PVLAB_GB07_DOTENV, as the SHA-256 of the value GB07 set.GB07
Budget for one billed preview per project per matching pull request.Each branch is billed for its own usage;3 with the setting off, 5 pull requests made 10 previews.GB01
Wait for the parent’s baseline table on a git-less branch before diffing or writing.ACTIVE_HEALTHY 1-2 s after create; the baseline table first appeared at 43-53 s (run 4), and a diff read in between proposed drop table for it (one trial by hand).BN01
Do not read GET /diff as a preview of what POST /merge applies on a git-less branch.A table, RLS, policy, privilege change, function, column and index written through /database/query were all in the diff and none reached the parent after 245-258 s.BN01, BN03
Generate supabase/migrations before merging; a merge applies history rows only.The same objects through POST /projects/{branch_ref}/database/migrations merged in 42-56 s with a history row, on the branch and merge where the /database/query set stayed out; MS15 saw 201 and no table 20 s later, no pg_net about 10 minutes later, after prisma db push. Migration files on a git-less branch were not measured.BN02, medium-serverless MS15d (2026-09-30)
Free a persistent branch with PATCH {persistent: false} before deleting it.The first DELETE answered 422 Cannot delete persistent branch.; after the PATCH the DELETE answered 200 and the branch was gone from the list after 1 s.medium-serverless MS15e (2026-09-30)
DocsRuntime measured
no mention of connecting more than one project to one repositorytwo projects connected with distinct working directories, both branching
”Supabase changes only” matches “Supabase files”, directory unstatedeach project previewed the pull requests changing a migration under its own <workdir>/supabase/
the wait examples filter on supabase/** and wait on Supabase Preview by namewith two projects, the wait read the other project’s run
”Supabase changes only” is described for branch creation; what happens to Supabase changes pushed after opening is unstateda migration pushed later created no preview until a close and reopen
Deploy to production is described per project; multiple projects on one repository are not mentionedboth connected projects ran production on a merge that changed only one app
secrets are branch-specific and read through env(); an env() with no value is not describedthe preview reached FUNCTIONS_DEPLOYED, every step EXITED, with the secret absent
the executor “will automatically retrieve and use” dotenvx .env.preview values; a key mismatch is not describedthe run died at clone and the branch status read MIGRATIONS_FAILED
blog, 2026-05-04: “Every schema change you make in the SQL Editor or Table Editor is tracked.”8untested for those editors: they reject a PAT. Through the Management API, a change written with /database/query left the branch history at remote_schema and stayed out of the merge
blog: “When you merge a branch, pg-delta compares the two schemas and generates the correct migration statements.”8not observed: the dashboard review diff was not opened. GET /diff default equalled pgdelta=false, which the OpenAPI document calls the Migra path, in 6 of 6 runs, and POST /merge applied history rows only
the MCP merge_branch tool is described as merging “migrations and edge functions from a development branch to production” (tool description as served 2026-10-10)consistent with the result: the apply_migration set merged and the execute_sql set did not, once by hand

None of these has been filed with Supabase as of 2026-09-29. The three rows added from BN01-BN03 were not filed either as of 2026-10-10 (filed: no). The blog’s “tracked” claim is not marked as wrong here: only the Management API routes were tested, and the dashboard routes could behave differently.

  • The runs used two pairs of projects, one pair per session, in one organization and one region, on one day. The per-commit counts could change without notice: 5 runs from a project that branched and 2 from one that did not. B posted 6 on the two-app pull request in GB01 with the setting on, and 5 in GB02 and GB03. On every commit read, every supabase app run was named Supabase Preview and every connected project posted to it.
  • Preview first-seen times in the GB01 runs are 30 s poll samples; creation latency was not measured. In GB04 the previews were first seen at 56 s on a 30 s poll, counted from when the run started creating the pull requests.
  • The late-change, re-seed and merge results are one pull request each.
  • GB01 with the setting off used an earlier revision of the scoping module that read only the default check-runs view. Its check-run figures come from a filter=all snapshot taken during that run and recorded in the RUNLOG, not from its published artifact.
  • The wait-by-name result depends on when the action’s first poll lands relative to the check-runs. GB02 recorded one pull request per shape on GitHub-hosted runners; a different start time can pick a different project’s run.
  • A single project on its own was not tested. In GB01 with the setting on, on the A-only pull request, A’s own skipped run came 29 s before its next run, and during those 29 s the name-filtered view showed only B’s skipped runs. The run therefore cannot say whether a wait step would ever read a lone project’s own skipped.
  • The Vercel integration was not part of the run.
  • The merge-without-migrations result in MS15 is from a different experiment and a different rig: one persistent branch created through the Management API with no git integration, a schema pushed by prisma db push, one merge, in one organization. BN01-BN03 repeated the shape on non-persistent branches with the SQL and migrations routes, two runs each, on 2026-10-10, and closed the open question for the Management API migrations route only: BN02 watched a merge of a schema written through POST /projects/{branch_ref}/database/migrations, and it applied. No one has watched a merge of a db push schema that DID carry migration files, or of migration files pushed any other way.
  • BN01-BN03 ran as three modules in parallel, each with its own parent, so six runs make two per module. The parent was polled for 245-258 s after the merge for the /database/query set; a later arrival is not excluded by that window, though MS15 read the parent about 10 minutes after. The by-hand observations (GET /projects/{branch_ref} 404, the drop table read, the MCP pair) are one trial each.
  • GB07 ran one pull request per secrets shape, on project A only. Not run: dotenvx with the key absent from the parent as well, the docs’ encrypted: literal directly in config.toml, a parent secret referenced through env(), [remotes.<name>] blocks on persistent branches, [db.vault] secrets, and a supabase secrets set on the parent after a preview is already open.

Measured claims map to module ids in experiments/github-branching; the run record is its RUNLOG.md, and the redacted artifacts with their measurement tables are under out/2026-09-23/. The second session’s RUNLOG entries and artifacts are at RUNLOG.md and out/2026-09-23/ as of 0c90777. The third session’s (GB07) are at RUNLOG.md and out/2026-09-29/ as of 6f55c11. The MS15 rows come from a different experiment on a different rig, with no git integration: medium-serverless, artifact under out/2026-09-30/. The BN rows (2026-10-10) come from experiments/branching-nogit, its RUNLOG.md and the redacted artifacts under out/2026-10-10/.

ClaimStatusHow it was checked
Two projects connected to one repository with working directories apps/a and apps/b; Branch limit 3 on both as connectedmeasuredGB00 - connection settings read back from GET /v2/organizations/{slug}/integrations/github/connections, in the published v2 OpenAPI document9
Setting off: 5 of 5 pull requests previewed on both projectsmeasuredGB01 - five pull requests opened together, both branch lists polled every 30 s for 600 s
Setting on: previews only for changes under the project’s <workdir>/supabase/measuredGB01 - same five shapes, second run
Runs per project on a commit, skipped runs from a project that did not branch, details_url as the only identifying fieldmeasuredGB01 - check-runs read with filter=all, attributed by the refs in details_url
Default view showed B’s skipped before A’s first in_progress on an A-only pull requestmeasuredGB01 - name-filtered default view sampled every 3 s for 150 s
Docs workflow for app A returned B’s skipped and B’s successmeasuredGB02 - the docs’ workflow in the repository, the check-run id read from the action’s log
An action run’s check_run_id points at a run left in_progressmeasuredGB02 - action runs on each preview project matched against the commit’s check-runs
Branch status per project on a failing and a passing preview; FUNCTIONS_DEPLOYED at 15 s before CREATING_PROJECTmeasuredGB03 - branch list and action runs sampled every 15 s for 480 s
Four kinds of file under A’s <workdir>/supabase/ trigger with the setting on; the bot comments once per project, the ignored one naming its directorymeasuredGB04 - four pull requests under apps/a/supabase/, both branch lists polled every 30 s for 600 s, comments read before cleanup
A migration pushed after opening needs a close and reopen; a pushed seed.sql change does not re-seedmeasuredGB05 - one pull request per question, 20 s poll, 240 s phases, the seed row read on the preview with SQL
An A-only merge started a production action run on both projectsmeasuredGB06 - squash merge, both parents’ action runs and migrations lists read for 240 s
A parent secret not referenced in config.toml absent from the preview; dotenvx value present; wrong key dies at clone with MIGRATIONS_FAILED; unset env() deploys with the secret absent; the list returns SHA-256 digests (2 of 2 checked)measuredGB07 - four pull requests under apps/a/supabase/, each adding a probe function; per name, the preview’s secrets list and the function’s environment compared by SHA-256, after a parent-side control
Decryption used the parent’s copy of DOTENV_PRIVATE_KEY_PREVIEWinferredGB07 - the key was absent from the three previews that returned readings; no shape ran without it on the parent
A branch with no git integration serves as isolated staging for a prisma db push schema, and diffs against the parentmeasuredMS15a-MS15c (2026-09-30) - create 201, ACTIVE_HEALTHY at 2 s, push exit 0 in 14859 ms, GET /branches/{id}/diff 200 with 2016 chars
A merge with no migration files applies nothing to the parentmeasuredMS15d, MS15d2 (2026-09-30) - the merge answered 201 and the parent’s pushed table and the branch’s pg_net extension were both absent (20 s and about 10 minutes after)
A git-less branch reads ACTIVE_HEALTHY 1-2 s after create, before the parent’s baseline table is on it (43-53 s, run 4)measuredBN01-BN03 - six runs, branch and workflow polled after create; the drop table diff in that window is one trial by hand
/database/query writes (table, RLS, policy, privileges, function, column, index) are in GET /diff in all three variants and absent from the parent after POST /mergemeasuredBN01, BN03 - two runs each; 7 of 7 writes in run 4 and in BN03 run 3, 5 of 7 in BN01 run 3; parent polled 245-258 s
The same objects through /database/migrations merge with a history rowmeasuredBN02 - one branch, both sets, one merge; first seen at 42 s (run 4) and 56 s (run 3); the /database/query set absent at 73 s and 87 s
request_review: true first changes nothingmeasuredBN03 - PATCH 200, review_requested_at set, merge outcome as BN01
execute_sql set stays out, apply_migration set merges (MCP)measuredby hand, one trial; not a harness module
The branch history of a prisma db push branch held only the baseline rowinferredfrom the matching outcome of MS15d and BN01; the Prisma path was not re-run
The Dashboard SQL Editor and Table Editor routes record a migration rownot runthey reject a PAT, so the blog’s “tracked” is untested for them
A persistent branch refuses DELETE until persistent: falsemeasuredMS15e (2026-09-30) - 422 Cannot delete persistent branch., then PATCH 200, then DELETE 200, gone after 1 s
The wait loop abovedesignnot run in CI
Automatic branching off on one project, its branches created from CInot runnot tested whether such a project still posts check-runs
ModuleExperimentTestArtifact
GB00github-branchinggb00-control.tsout/2026-09-23
GB01github-branchinggb01-pr-scoping.tsout/2026-09-23
GB02github-branchinggb02-wait-by-name.tsout/2026-09-23
GB03github-branchinggb03-per-project-signal.tsout/2026-09-23
GB04github-branchinggb04-supabase-file-kinds.tsout/2026-09-23
GB05github-branchinggb05-late-changes.tsout/2026-09-23
GB06github-branchinggb06-merge-fanout.tsout/2026-09-23
GB07github-branchinggb07-branch-secrets.tsout/2026-09-29
BN01branching-nogitbn01-sql-path.tsout/2026-10-10
BN02branching-nogitbn02-migration-path.tsout/2026-10-10
BN03branching-nogitbn03-merge-request.tsout/2026-10-10
MS15medium-serverlessms15-branch-prisma.tsout/2026-09-30
  1. Supabase, “GitHub integration,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/github-integration ↩ ↩2 ↩3 ↩4

  2. Supabase, “Working with branches,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/working-with-branches ↩ ↩2 ↩3 ↩4

  3. Supabase, “Manage Branching usage,” Supabase Docs. https://supabase.com/docs/guides/platform/manage-your-usage/branching ↩ ↩2

  4. GitHub, “REST API endpoints for check runs,” GitHub Docs. https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference ↩

  5. fountainhead, “src/poll.ts at v1.2.0,” action-wait-for-check. https://github.com/fountainhead/action-wait-for-check/blob/v1.2.0/src/poll.ts ↩

  6. Supabase, “Management API OpenAPI document (BranchResponse_Output.status),” api.supabase.com. https://api.supabase.com/api/v1-json ↩ ↩2 ↩3

  7. Supabase, “Configuration,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/configuration ↩

  8. Supabase, “Branching Without Git Is Now The Default,” Supabase Blog, 2026-05-04. https://supabase.com/blog/branching-without-git-is-now-the-default ↩ ↩2

  9. Supabase, “Management API v2 OpenAPI document,” api.supabase.com. https://api.supabase.com/api/v2-json ↩