Moving Supabase projects under one organization
You have several Supabase accounts - one per client, or one per project, usually because the Free Plan caps you at two active projects. Now you want them under a single organization you can bill and administer in one place.
This changes the billing and administrative container only. Each project stays its own Postgres instance with its own keys, so tenancy does not change. If you want fewer projects rather than fewer organizations, that decision is in where should a tenant live.
Moving a project between organizations needs no pg_dump into a fresh project and
no auth-config rebuild. It is a first-class operation that keeps the project’s ref, URL, API keys, signing keys,
storage, Edge Functions and auth config exactly as they are, and it runs entirely on
the public Management API.
It is also the same primitive Supabase documents for platforms handing a project to
an end user’s own organization - see
Supabase for Platforms
and GET /v1/oauth/authorize/project-claim, which wraps it in an OAuth consent flow.
This guide takes the direct API path, for the case where you own both organizations.
Measurement provenance
Section titled “Measurement provenance”Numbers here were measured on 2026-08-03 against live Supabase projects in
ap-southeast-1 on Micro compute, moving between a Team-plan, a Pro-plan and a
Free-plan organization. Every measured claim in the evidence table at the end was
executed and captured; anything sourced from documentation rather than a run is
labelled as such in that table. Project identifiers are replaced with
<source-ref> and <target-ref> throughout.
Constants
Section titled “Constants”| Fact | Value |
|---|---|
| Transfer endpoint pair | POST /v1/projects/{ref}/claim-token, then POST /v1/organizations/{slug}/project-claim/{token} |
| Dry-run endpoint | GET /v1/organizations/{slug}/project-claim/{token} |
| Claim token lifetime | 1 hour (response expires_at; measured) |
| Measured claim API-call duration | 2.3 s paid to paid, 4.0 s paid to Free - the API call only; the paid-to-Free data-plane outage is the 75.2 s of Part 5 |
| Free Plan project cap | 2 active projects, counted per member across every organization where that member is Owner or Administrator |
| Pro Plan cost | $25 per organization per month, plus compute |
| Micro compute | $0.01344/hour, about $10/month |
| Compute credit | $10 per paid organization per month |
| Minimum compute on a paid plan | Micro - Nano is refused at creation |
| Management API base | https://api.supabase.com/v1 |
Everything below assumes a Personal Access Token (PAT) in $SUPABASE_ACCESS_TOKEN.
Decide the topology before you move anything
Section titled “Decide the topology before you move anything”An organization is a billing and access boundary. Data isolation comes from the project: every project is its own Postgres instance with its own API keys and signing keys, so per-client data isolation is the same whether those projects sit in one organization or ten.
What the organization does share:
| Shared at the organization level | Consequence |
|---|---|
| Billing and invoicing | One invoice covering every client |
| Usage quotas | Egress, storage and monthly active users (MAU) pool across all projects, so one client’s spike consumes everyone’s allowance |
| Fair-use restrictions | Applied to the organization, so an overdue invoice affects every project in it |
| Member roles | Owner and Administrator reach all current and future projects |
| Plan | One plan per organization; projects cannot be on different plans within it |
There is no parent/child organization concept, every organization needs at least one Owner, and Owner and Administrator are organization-scoped. There is no role meaning “administrator of this one project, blind to the others” unless you are on Team or Enterprise.
| Your situation | Topology |
|---|---|
| Clients never touch the dashboard; you operate everything and bill centrally | One organization, one project per client |
| Clients need dashboard access scoped to their own project | Team plan (project-scoped roles) or one organization per client |
| Clients need separate invoices | One organization per client |
| A client may take ownership of their project later | One organization per client - though a transfer makes this a one-command change either way |
| Different clients need different plans | One organization per client; plans do not mix |
A single organization costs less: $25 per organization per month plus roughly $10 per project of compute, with one $10 compute credit per organization. Three projects on Pro comes to about $45/month in one organization, and about $75/month split across three.
The plan matrix, read from the API
Section titled “The plan matrix, read from the API”Rather than trusting a pricing page, read the entitlements for an organization directly:
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/organizations/<slug>/entitlements" | jqDifferences that affect this decision, measured on a Pro and a Team organization on 2026-08-03:
| Entitlement | Pro | Team |
|---|---|---|
project_scoped_roles | false | true |
security.member_roles | Owner, Administrator, Developer | plus Read-only |
security.audit_logs_days | 0 | 62 |
log.retention_days | 7 | 28 |
backup.retention_days | 7 | 14 |
auth.platform.sso | false | true |
audit_log_drains | false | true |
function.max_count | 500 on 2026-08-03; 1000 on 2026-09-02 | 1000 on 2026-08-03; 2000 on 2026-09-02 |
project_pausing | false | false |
api.members.roles | false | false |
The function.max_count row moved between reads. On 2026-09-02 (platform-facts F01,
against a Free, a Pro and a Team organization) it read Free 100, Pro 1000, Team 2000, and
Supabase’s docs raised the Pro and Team figures to 1000 and 2000 in a commit dated
2026-08-18.1 security.member_roles (Read-only on Team only),
auth.platform.sso (Team only) and security.audit_logs_days (Pro 0, Team 62) read the
same on both dates; the remaining rows were not re-read.
project_pausing is false on both
paid plans shown - a Pro or Team project cannot be paused, so an idle client project
bills until you delete it or move it to a Free organization. The platform plan is
the exception: project_pausing reads true there and pause is enforced (200 ->
INACTIVE; sfp-platforms S06, 2026-08-24/25). And api.members.roles is false on
both, so neither organization can manage members through the Management API: v1
only lists members, and the v2 role and invitation writes are listed for Enterprise
only (x-allowed-plans). On these plans member management is dashboard-only.
Why you are on multiple accounts in the first place
Section titled “Why you are on multiple accounts in the first place”The Free Plan cap counts per member across organizations. Creating a third free project returns:
The following organization members have reached their maximum limits for the numberof active free projects within organizations where they are an administrator orowner: <member> (2 project limit). To continue, these users will need to eitherdelete, pause or upgrade one or more of these projects.Because the count follows the member across every organization where they are Owner or Administrator, making a second organization does not give you two more free projects. A second account does, which is how agencies end up with one login per client. Once the destination organization is on a paid plan this stops applying.
Part 1: prepare
Section titled “Part 1: prepare”Pick the destination organization and put it on a paid plan first. Transferring into a Free organization is both slower and constrained by the two-project cap. If the destination is already Pro or Team, both problems disappear.
Get the operator into both sides. You need to be Owner of the source
organization and at least a member of the destination. With separate accounts per
client, the cheapest path is to invite the account that will own the consolidated
organization into each source organization as Owner, transfer, then remove the
temporary membership. The API’s member-role writes are listed for Enterprise only (x-allowed-plans), so on other plans this part is clicking.
Clear the blockers. A project will not transfer with an active GitHub integration connection, a project-scoped role pointing at it, or log drains configured. Remove those first.
Check for name collisions. Project names must be unique within an organization, and a transfer into an organization that already has that name is a conflict to find before the window. Rename on the source side beforehand.
Enumerate what you are moving.
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects" \ | jq -r '.[] | "\(.ref)\t\(.name)\t\(.region)\t\(.organization_id)\t\(.status)"'Note the regions: a transfer cannot change region. If a project also needs to move region, that is a different and much larger operation - see the region migration guide. Do the region move first, then transfer the resulting project.
Part 2: dry-run every project
Section titled “Part 2: dry-run every project”The GET on the claim path is a validator. It tells you the plan pair, whether the
transfer is valid, and what will change - without moving anything.
REF=<source-ref>DEST=<destination-org-slug>
TOKEN=$(curl -s -X POST -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF/claim-token" | jq -r .token)
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/organizations/$DEST/project-claim/$TOKEN" \ | jq '.preview | {source_subscription_plan, target_subscription_plan, valid, errors: [.errors[]?.key], warnings: [.warnings[]?.key], over: .members_exceeding_free_project_limit}'A valid Free-to-Pro move looks like this:
{ "source_subscription_plan": "free", "target_subscription_plan": "pro", "valid": true, "errors": [], "warnings": ["WARN_COMPUTE_COSTS_INCREASE_IN_TARGET_ORGANIZATION"], "over": []}Warnings and errors you will see:
| Key | Meaning |
|---|---|
WARN_COMPUTE_COSTS_INCREASE_IN_TARGET_ORGANIZATION | The destination already has projects, so its compute credit is spent; this project adds about $10/month |
WARN_TARGET_ORGANIZATION_NOT_ON_USAGE_BILLING | Destination is not on usage-based billing |
WARN_TARGET_ORGANIZATION_ON_FREE_PLAN_AND_PROJECT_ON_HIGHER_COMPUTE | The instance will be downgraded to Nano, with downtime - see Part 5 |
INFO_NO_AUTOMATIC_COMPUTE_UPGRADE | Landing in a paid organization entitles you to a free Micro upgrade, but you have to trigger it |
ERR_TARGET_ORGANIZATION_EXCEEDS_FREE_PROJECT_LIMIT | valid is false; the destination is a Free organization and this would put a member over two projects |
Run this for every project before you move the first one. If you decide not to proceed, revoke the token rather than leaving it live for its hour:
curl -s -X DELETE -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF/claim-token"Part 3: transfer
Section titled “Part 3: transfer”Same token, same path, POST:
curl -s -X POST -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/organizations/$DEST/project-claim/$TOKEN"Measured at 2.3 s for a paid-to-paid move. Confirm:
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF" | jq '{organization_id, status}'Because ref, URL and keys are unchanged, there is nothing to redeploy and no client to repoint. The loop over several projects:
for REF in <ref-1> <ref-2> <ref-3>; do TOKEN=$(curl -s -X POST -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF/claim-token" | jq -r .token) VALID=$(curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/organizations/$DEST/project-claim/$TOKEN" | jq -r .preview.valid) if [ "$VALID" != "true" ]; then echo "SKIP $REF - preview invalid" curl -s -X DELETE -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF/claim-token" >/dev/null continue fi curl -s -o /dev/null -w "$REF -> %{http_code}\n" -X POST \ -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/organizations/$DEST/project-claim/$TOKEN" sleep 15doneThe sleep is rate-limit courtesy. In the rate-limits L01 burst against a cheap
scoped read (2026-08-17), the API answered request 118 of a 120 budget with its own
JSON 429 ThrottlerException and retry-after: 60. An HTML error page appeared on
aggressive polling of other endpoints, where a client that assumes JSON throws a
parse error instead of seeing something retryable. A later burst matrix (rate-limits
L02, 2026-10-13: five read endpoints, each burst serially, 20 in parallel and as a
150-request spike) got the JSON throttler in all 15 cells and never the HTML page, so
that page is real but not reproducible on demand. Every response carries
x-ratelimit-remaining; pace by it and treat a non-JSON body as the same signal.
Part 4: the token that does all of this
Section titled “Part 4: the token that does all of this”A classic PAT has no scopes. It reads and writes every organization and every project the account can reach, including creating organizations and deleting projects.
On 2026-08-03 there was no token-scoping surface on the API: /v1/oauth/apps,
/v1/profile/permissions, /v1/tokens and /v1/profile/access-tokens all returned
404, while /v1/organizations and /v1/profile returned 200 on the same token. Every
documented endpoint advertises Auth: bearer | fga_permissions. As of 2026-10-07 the
Personal Access Tokens guide documents scoped tokens, created in the dashboard and
limited to the organizations, projects and permissions you choose; the dashboard
labels the older kind “Legacy”.2 A scoped token was not measured here.
Consolidating into one organization still widens what one leaked classic token
reaches, from one client to every client. The mitigations: a scoped token for each
job, a dedicated automation account, short-lived tokens, and rotating after a
migration run. Per-credential
scoping exists only at the data plane: POST /v1/projects/{ref}/api-keys accepts a
secret_jwt_template binding a secret key to a role. On a platform-plan org the
template is accepted and echoed (201, measured 2026-08-24). The minted key is
opaque, but exchanging it at the data plane (measured 2026-08-25, Pro org) shows the
claims do reach the token - a probe RPC returning auth.jwt() reports the templated
role and custom claim. It is still not a substitute for Management API scoping.
Part 5: the one direction that costs you downtime
Section titled “Part 5: the one direction that costs you downtime”Transfers between paid organizations produced no data-plane interruption
distinguishable from the ambient noise floor of the probing host, measured by polling
/auth/v1/health every 250 ms across each move.
Moving a project from a paid organization to a Free one costs downtime, because a Free organization only offers Nano compute and the instance is resized on arrival:
| Direction | Measured data-plane outage |
|---|---|
| Team to Pro | none above the noise floor |
| Pro to Team | none above the noise floor |
| Free to Pro | none above the noise floor |
| Paid to Free | 75.2 s continuous |
During that window the edge returns HTTP 521 and 525 rather than a clean 503, so
retry logic keyed on origin 5xx will not classify it correctly. The project reports
status: RESIZING throughout.
Part 6: compute after the move
Section titled “Part 6: compute after the move”A Free project keeps Nano compute when it lands in a paid organization. It is not
upgraded automatically, and Nano is not otherwise purchasable there - creating a
project with Nano on a paid plan is refused with
400 {"message":"Minimum instance size on paid plans is Micro"}.
That refusal is for an explicit desired_instance_size. Supabase’s platform guidance
is to omit the field entirely to land on Nano, and notes that scale-to-zero pricing
applies to Nano only - Micro and above cannot scale to zero. Access is granted per
account rather than being generally available: on a platform-plan org (measured
2026-08-24) the SfP-prescribed create provisions Nano by default and pause is
enforced; on Pro and Team orgs there is no idle discount. If your reason for
consolidating is the cost of idle client projects, that is the programme to ask
about before you optimise around it.
The preview says the project adds about $10/month to the bill, which is the Micro rate. You are paying for Micro and running Nano until you act, and the upgrade itself is free, per the Compute and Disk guide:
Transferring the project to a paid organization allows a free upgrade to a Micro compute size. You can manually trigger the upgrade in the Compute & Disk section after the transfer.
The only cost of taking it is the resize downtime. Over the API the upgrade is
PATCH /v1/projects/{ref}/billing/addons with
{addon_variant: "ci_micro", addon_type: "compute_instance"} - compute is an addon
mutation, and there is no resize endpoint. Check what a project is currently on:
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ "https://api.supabase.com/v1/projects/$REF/billing/addons" \ | jq '{selected: [.selected_addons[]? | {type, name: .variant.name, price: .variant.price.description}]}'An empty selected_addons means Nano. A Micro project shows a compute_instance
add-on at $0.01344/hour.
What to do about it
Section titled “What to do about it”A practice with no module id is a design choice rather than a result; the documented-not-tested rows in the evidence table stay that way.
| Practice | Evidence | Module |
|---|---|---|
Replace the loop’s sleep 15 with header-paced backoff: read x-ratelimit-remaining, honour retry-after: 60. | The throttle answers 429 ThrottlerException with a JSON body; a non-JSON body is the same signal. The limit is 120 per user per scope, and a scoped read decrements it 1:1. | rate-limits L01, L01b (2026-08-17/18) |
| Do not shard the migration across PATs. | Two PATs from one user drain one counter. | rate-limits L01, L01b (2026-08-17/18) |
| Run the migration from a dedicated automation user. | A separate user is a separate 120-per-minute bucket, and its PAT can be revoked after the run without touching a human’s; a classic PAT reaches the user’s whole membership set, so scope the run’s token where the job allows it.2 | rate-limits L01b (2026-08-18); platform-facts F03 |
Read entitlements by feature key rather than dumping the body: jq '.entitlements[] | select(.feature.key == "project_pausing") | .hasAccess'. | The payload is a flat list, {entitlements: [{feature: {key, type}, hasAccess, config: {...}}]}, 64 features on Free, Pro and Team on 2026-09-02; the plan label is on GET /v1/organizations/{slug} (plan), not in the entitlements body. | platform-facts F01 (2026-09-02) |
| Date every numeric entitlement you plan around and re-read it before the move. | The function.max_count reading moved: Pro 500 and Team 1000 on 2026-08-03, then Free 100, Pro 1000, Team 2000 on 2026-09-02; security.audit_logs_days read 0 and 62 on both dates. | platform-facts F01 (2026-09-02); this doc’s 2026-08-03 read |
If the destination is a platform-plan organization, plan around project_pausing being available. | The entitlement reads true and pause is enforced (200 -> INACTIVE, restore reverses it). The Pro and Team false rows do not apply there. | sfp-platforms S06 (2026-08-24/25) |
Take the free Micro upgrade with PATCH /v1/projects/{ref}/billing/addons, and budget the window off the resize figures, not the restart ones. | The request body is {addon_variant: "ci_micro", addon_type: "compute_instance"}. platform-downtime measured one Micro project at Auth 131 s resizing up against 75 s for a restart, and pooler 207 s against 158 s - not this doc’s 75.2 s paid-to-Free figure, which is a resize down to Nano. An adjacent addon PATCH answers 429 "still processing addon changes". | supabase-lab AGENTS.md, platform-downtime (2026-08-04); compute-disk D09 (2026-08-19); sfp-platforms S07 (2026-08-25) |
During a paid-to-Free move, classify edge HTTP 521 and 525 as retryable. | The window is 75 s; origin-5xx retry logic does not match them, and the project reports status: RESIZING throughout. | this doc’s 250 ms probe (2026-08-03) |
Before creating a destination project for a region-then-transfer path, enumerate the legal regions with GET /v1/projects/available-regions?organization_slug=<slug>. | The call answers 200 with {recommendations, all: {smartGroup[], specific[]}}; a call without the slug answers 400; /regions and /projects/regions 404. | platform-facts F04c (2026-08-20) |
Verification
Section titled “Verification”| Check | How | Expected |
|---|---|---|
| Project moved | GET /v1/projects/{ref} | organization_id is the destination |
| Ref unchanged | compare before and after | identical - no client repointing |
| Keys unchanged | GET /v1/projects/{ref}/api-keys | all four keys identical (legacy anon, legacy service_role, publishable, secret) |
| Signing keys unchanged | GET /v1/projects/{ref}/config/auth/signing-keys | same key ids and algorithms |
| Auth config unchanged | GET /v1/projects/{ref}/config/auth | identical |
| Data intact | select count(*) on a known table | unchanged |
| Storage intact | fetch a known object through the Storage API | same bytes |
| Edge Functions intact | invoke a deployed function | same response |
| Vault intact | select decrypted_secret from vault.decrypted_secrets | plaintext, no error |
| Data plane healthy | GET https://<ref>.supabase.co/auth/v1/health | 200 - poll this, not project status |
| Compute as intended | GET /v1/projects/{ref}/billing/addons | Micro add-on present if you took the free upgrade |
| Source organization empty | GET /v1/projects | no projects left under the old organization |
What a transfer preserves
Section titled “What a transfer preserves”Captured as a 15-field fingerprint before and after a Team-to-Pro-to-Team round trip.
The only field that changed was organization_id; the round trip returned
byte-identical on all fifteen.
| Preserved | Not preserved |
|---|---|
| Project ref and URL | Organization membership (the point of the exercise) |
| All four API keys | Your effective rights, which follow your role in the destination |
| Signing key ids and algorithms | |
| pgsodium encryption root key | |
| Auth config | |
| PostgREST config | |
| Database rows and roles | |
| Storage buckets and object bodies | |
| Edge Function bodies and secrets | |
| Region (it cannot be changed) |
This is the difference from a dump-and-restore. A restore into a fresh project gives you a new ref, new keys, new signing keys and a new encryption root key, which means repointing every client, re-registering every OAuth callback, and losing any Vault secret you did not explicitly carry across. A transfer changes one field.
Gotchas and lessons learned
Section titled “Gotchas and lessons learned”/v1/projects/{ref}/transferreturns 404. There is no endpoint by that name, which is what leads people to conclude the operation is dashboard-only. The path is the claim-token pair.- Preview every move. The warning set differs by plan pair. Free-to-Pro warns about cost; paid-to-Free warns about a compute downgrade and its downtime; Pro-to-Team warned about nothing at all.
ACTIVE_HEALTHYis not “ready”. The control plane reported healthy about 19 seconds before the data plane served, in one run on the aggregate status.- A partial SMTP
PATCHwipes SMTP. Setting the full block and later patchingsmtp_passalone leftsmtp_host,smtp_userandsmtp_passall null on the next read. Send the whole block or none of it. It happens during post-move config cleanup, after the transfer itself. - Credential fields in
/config/authare neither null nor readable. Once set, SMTP passwords, OAuth client secrets and the captcha secret read back as a stable 64-character hex digest. The same plaintext written to a different project produces a different digest, so the fingerprint is project-scoped: you cannot read a secret out to copy it, and you cannot confirm a copy landed by comparing digests either. Re-enter by hand and verify out of band. A transfer does not move the config, so this only matters for a dump-and-restore. jwt_secretis not exposed byGET /v1/projects/{ref}/config/auth. New projects sign with an ES256 key with the legacy HS256 secret demoted topreviously_used, so the widely repeated advice to “copy the JWT secret to the new project to keep sessions alive” does not apply to a new project and cannot be scripted on/v1anyway. A transfer keeps the signing keys, so it does not arise there.- Rate limiting does not always arrive as JSON. The measured breach on a scoped
read was a JSON
429 ThrottlerException(rate-limits L01, 2026-08-17), but aggressive polling of other endpoints has earned a Cloudflare HTML interstitial. Back off on either, and check the content type before parsing. - Organizations can be created on
/v1but not deleted there. Cleanup of an emptied source organization is a dashboard action.
Evidence
Section titled “Evidence”Measured 2026-08-03 unless marked otherwise.
| Claim | How it was checked | Result |
|---|---|---|
| Transfer runs on the public Management API | Executed the claim-token pair with a PAT, no dashboard involved | Measured - 2.3 s paid to paid |
| Transfer preserves ref, keys, signing keys, root key, config, data, storage, functions | 15-field fingerprint diffed pre/post across a Team-Pro-Team round trip | Measured - only organization_id changed |
| Paid-to-paid transfer causes no outage | Polled /auth/v1/health at 250 ms across three moves | Measured - nothing above the host’s ambient failure floor |
| Paid-to-Free costs about 75 s | Same probe across the move | Measured - 75.2 s continuous, HTTP 521/525 |
| Free Plan cap is per member across organizations | Created free projects until refused | Measured - verbatim API error |
| The cap is enforced at transfer time too | Previewed a third project into a Free organization at the cap | Measured - valid: false, ERR_TARGET_ORGANIZATION_EXCEEDS_FREE_PROJECT_LIMIT |
| Nano is refused at creation on a paid plan | Attempted it | Measured - 400 Minimum instance size on paid plans is Micro |
| A transferred-in Free project keeps Nano | Compared billing/addons against a Micro project | Measured - no compute add-on selected |
| Nano in a paid organization bills at the Micro rate | Read the preview warning | Measured - “will increase by $10/month” |
| Micro is $0.01344/hour | GET /v1/projects/{ref}/billing/addons | Measured |
| Plan matrix (project-scoped roles, Read-only, pausing, API member management) | GET /v1/organizations/{slug}/entitlements on a Pro and a Team organization | Measured 2026-08-03; function.max_count, security.member_roles, auth.platform.sso and security.audit_logs_days re-read 2026-09-02 (platform-facts F01) - only function.max_count moved |
No token-scoping endpoint on /v1 | Probed every plausible scope/permission endpoint against a working token | Measured 2026-08-03 - all 404 while normal endpoints return 200; scoped tokens have since been documented as a dashboard feature (read 2026-10-07), not measured here |
| Per-service health is a separate endpoint | GET /v1/projects/{ref}/health?services=... on a live project | Measured - four services reporting independently |
| Control plane leads the data plane by ~19s | Compared aggregate project status against the data plane | Observed, method superseded - not re-measured against the per-service endpoint |
| Scale-to-zero on Nano | Ran the SfP-prescribed create (no desired_instance_size) plus a pause/restore cycle on a platform-plan organization, 2026-08-24 | Measured - create provisions nano by default and pause is enforced (200 -> INACTIVE); nano is the platform plan’s create default, not a catalogue variant |
Secret keys can be bound to a role via secret_jwt_template | POST /v1/projects/{ref}/api-keys with a template on a platform-plan organization, 2026-08-24; exchanged at the data plane on a Pro org, 2026-08-25 | Measured - accepted (201) and echoed; the minted key is opaque (sb_secret_...) and the create response redacts it without ?reveal=true, but the exchanged token carries the templated role and custom claim (probe RPC returning auth.jwt()) |
Fine-grained authorization (fga_permissions) | Advertised on every endpoint; no surface reachable with a PAT | Not tested |
| Credential fields return a project-scoped digest | Wrote the same secret to two projects and compared | Measured |
Partial SMTP PATCH clears SMTP | Set the full block, patched one field, re-read | Measured |
| A GitHub integration blocks transfer | Not run - creating the connection needs the dashboard OAuth flow | Documented, not tested |
| A project-scoped role blocks transfer | Not run - the API’s role-assignment writes are listed for Enterprise only | Documented, not tested |
| Quotas pool organization-wide | Not run - usage endpoints are not on /v1 | Documented, not tested |
| Fair-use restriction covers all projects in an organization | Not run - deliberately, tripping fair use on a live account is out of scope | Documented, not tested |
| An organization-scoped role covers future projects | Not run - needs dashboard member management | Documented, not tested |
| Rights follow your role in the destination organization | Not run - needs a second account | Documented, not tested |
| Source organization is billed up to the transfer, destination after | Not run - needs two full billing cycles | Documented, not tested |
Modules
Section titled “Modules”| Module | Experiment | Test | Artifact |
|---|---|---|---|
| D09 | compute-disk | d09-compute-resize-timing.ts | none published |
| F01 | platform-facts | f01-entitlements.ts | none published |
| F03 | platform-facts | f03-pat-scope.ts | none published |
| F04c | platform-facts | f04-regions.ts | none published |
| L01 | rate-limits | r01-rate-limit-surface.ts | none published |
| L01b | rate-limits | r01-rate-limit-surface.ts | none published |
| L02 | rate-limits | l02-burst-shapes.ts | run-2026-10-13T05-09-36-511Z |
| S06 | sfp-platforms | s06-platform-entitlements.ts | none published |
| S07 | sfp-platforms | s07-read-replicas.ts | none published |
File reference
Section titled “File reference”Nothing on disk. Every step is a curl against the Management API (https://api.supabase.com/v1) with a personal access token in $SUPABASE_ACCESS_TOKEN, plus jq to read the JSON. The measurements behind the constants and the practices table live in the supabase-lab experiments listed under Modules.
Related
Section titled “Related”- Migrating a Supabase project to another region
- when the project also has to change region. Do that first, then transfer.
- Running many tenants on one Supabase project
- when one project per client is the wrong shape and you want the idle majority to share an instance.
References
Section titled “References”-
Supabase, “docs(functions): update Pro and Team function limits (#49173),” supabase/supabase commit 7107a22, 2026-08-18. https://github.com/supabase/supabase/commit/7107a22a6707c06546ee6d62aedb77da9c75b16a ↩
-
Supabase, “Personal Access Tokens,” Supabase Docs. https://supabase.com/docs/guides/platform/personal-access-tokens (read 2026-10-07). ↩ ↩2