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.
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.mdchange 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; aseed.sql,config.toml, function orNOTES.mdchange 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 oneskippedrun; a project that does not branch posts 2, bothskipped. - The docs’ wait-by-check-name workflow, path-filtered to app A, read project
B’s run both times: B’s
skippedon an A-only pull request, which skipped the migrate job, and B’ssuccesson a pull request touching both apps. - The Management API branch list, which is per project, separated the
outcomes:
MIGRATIONS_FAILEDfor A andFUNCTIONS_DEPLOYEDfor B on the same pull request. The first sample, at 15 s, readFUNCTIONS_DEPLOYEDon both, beforeCREATING_PROJECTappeared 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.
- One pull request’s head commit reaches both projects through the one GitHub App.
- Each project decides on its own whether to create a preview branch.
- Both projects post their check-runs to the same commit under the same name, whether they branch or not.
Which setup to pick
Section titled “Which setup to pick”| Goal | Setup | Status |
|---|---|---|
| A pull request that touches only app A creates only A’s preview | ”Supabase changes only” on, on both projects | measured |
| Every pull request gets a preview on both projects | ”Supabase changes only” off | measured; 2 previews per pull request, each billed for its own usage3 |
| A CI job waits for app A’s preview before seeding or testing it | poll project A’s branch list for the pull request’s git branch | branch-list signal measured; the wait loop below was not run in CI |
A CI job waits on the Supabase Preview check by name | do not use with two projects connected | measured: it read the other project’s run |
| Only one project reports on a commit | Automatic branching off on one project; create its branches from CI | not 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 changes | Off: preview on A | Off: preview on B | On: preview on A | On: preview on B |
|---|---|---|---|---|
README.md at the repository root | yes | yes | no | no |
apps/a/README.md | yes | yes | no | no |
a migration under apps/a/supabase/ | yes | yes | yes | no |
a migration under apps/b/supabase/ | yes | yes | no | yes |
| a migration under each | yes | yes | yes | yes |
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 changed | Preview on A | Preview on B |
|---|---|---|
seed.sql (a second insert) | yes | no |
config.toml (a comment line appended) | yes | no |
functions/probe/index.ts (a new function) | yes | no |
NOTES.md (a file the CLI does not read) | yes | no |
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
The checks on a shared commit
Section titled “The checks on a shared commit”With two projects connected, both projects’ check-runs land on the same commit.
| On the head commit | Measured |
|---|---|
| Check-run name | Supabase Preview on every run, from both projects |
| GitHub App and check suite | supabase for every Supabase Preview run; one check suite on the setting-off commits (RUNLOG snapshot) |
| Runs from a project that branched | 5, 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 project | details_url only, of external_id, details_url, output title and output summary |
external_id | empty on every run |
| Output title | Supabase 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.
Waiting for one project’s preview in CI
Section titled “Waiting for one project’s preview in CI”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 request | Wait step returned | That run belonged to | Migrate job | A’s first success seen at |
|---|---|---|---|---|
migration under apps/a only | skipped | project B | skipped | 108 s |
| migration under each app | success | project B | ran | 108 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 status | B branch status |
|---|---|---|
| 15 | FUNCTIONS_DEPLOYED | FUNCTIONS_DEPLOYED |
| 32 | CREATING_PROJECT | CREATING_PROJECT |
| 65 | RUNNING_MIGRATIONS | RUNNING_MIGRATIONS |
| 81 | MIGRATIONS_FAILED | FUNCTIONS_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.
# Needs SUPABASE_ACCESS_TOKEN and this app's parent project ref.seen_busy=0for 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 10doneexit 1MIGRATIONS_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.
Merging to the production branch
Section titled “Merging to the production branch”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 merge | Project A | Project B |
|---|---|---|
| New production action runs | 1 | 1 |
| Steps | clone, pull, health, migrate, seed, deploy EXITED; configure PAUSED | the same |
| The merged migration in the project’s migrations list | yes | no |
supabase app check-runs on the merge commit, at the end of the watch | 3: success + 2 in_progress | 3: 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
What to do about it
Section titled “What to do about it”| Practice | Evidence | Module |
|---|---|---|
| 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 |
| 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 |
Where the docs are silent
Section titled “Where the docs are silent”| Docs | Runtime measured |
|---|---|
| no mention of connecting more than one project to one repository | two projects connected with distinct working directories, both branching |
| ”Supabase changes only” matches “Supabase files”, directory unstated | each 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 name | with 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 unstated | a 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 mentioned | both connected projects ran production on a merge that changed only one app |
None of these has been filed with Supabase as of 2026-09-23.
Reading the numbers
Section titled “Reading the numbers”- 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
supabaseapp run was namedSupabase Previewand 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=allsnapshot 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
skippedrun came 29 s before its next run, and during those 29 s the name-filtered view showed only B’sskippedruns. The run therefore cannot say whether a wait step would ever read a lone project’s ownskipped. - The Vercel integration was not part of the run.
Evidence, by module
Section titled “Evidence, by module”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.
| Claim | Status | How it was checked |
|---|---|---|
Two projects connected to one repository with working directories apps/a and apps/b; Branch limit 3 on both as connected | measured | GB00 - connection settings read back from GET /v2/organizations/{slug}/integrations/github/connections, in the published v2 OpenAPI document7 |
| Setting off: 5 of 5 pull requests previewed on both projects | measured | GB01 - 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/ | measured | GB01 - 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 field | measured | GB01 - 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 request | measured | GB01 - name-filtered default view sampled every 3 s for 150 s |
Docs workflow for app A returned B’s skipped and B’s success | measured | GB02 - 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_progress | measured | GB02 - 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_PROJECT | measured | GB03 - 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 directory | measured | GB04 - 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-seed | measured | GB05 - 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 projects | measured | GB06 - squash merge, both parents’ action runs and migrations lists read for 240 s |
| The wait loop above | design | not run in CI |
| Automatic branching off on one project, its branches created from CI | not run | not tested whether such a project still posts check-runs |
Modules
Section titled “Modules”| Module | Experiment | Test | Artifact |
|---|---|---|---|
| GB00 | github-branching | gb00-control.ts | out/2026-09-23 |
| GB01 | github-branching | gb01-pr-scoping.ts | out/2026-09-23 |
| GB02 | github-branching | gb02-wait-by-name.ts | out/2026-09-23 |
| GB03 | github-branching | gb03-per-project-signal.ts | out/2026-09-23 |
| GB04 | github-branching | gb04-supabase-file-kinds.ts | out/2026-09-23 |
| GB05 | github-branching | gb05-late-changes.ts | out/2026-09-23 |
| GB06 | github-branching | gb06-merge-fanout.ts | out/2026-09-23 |
Related docs
Section titled “Related docs”- Supabase preview-branch compute sizing - what each preview costs and how to size it, including creating branches from CI.
- Detach a Supabase branch from its git branch - stopping a persistent branch’s redeploys without deleting it.
References
Section titled “References”-
Supabase, “GitHub integration,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/github-integration ↩ ↩2 ↩3 ↩4
-
Supabase, “Working with branches,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/working-with-branches ↩ ↩2 ↩3 ↩4
-
Supabase, “Manage Branching usage,” Supabase Docs. https://supabase.com/docs/guides/platform/manage-your-usage/branching ↩ ↩2
-
GitHub, “REST API endpoints for check runs,” GitHub Docs. https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference ↩
-
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 ↩
-
Supabase, “Management API OpenAPI document (BranchResponse_Output.status),” api.supabase.com. https://api.supabase.com/api/v1-json ↩ ↩2
-
Supabase, “Management API v2 OpenAPI document,” api.supabase.com. https://api.supabase.com/api/v2-json ↩