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 it is not a tenancy change - if what you actually want is fewer projects rather than fewer organizations, that decision is in where should a tenant live.
The common assumption is that this means pg_dump into fresh projects, plus
rebuilding auth config. It does not. Moving a project between organizations 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.
What follows is the direct API path for the case where you own both organizations.
This guide covers the topology decision first, because it determines how many organizations you end up with, then the runbook.
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”Fixed facts every later step depends on.
| 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 transfer time | 2.3 s paid to paid, 4.0 s paid to Free |
| 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. It is not a data boundary - 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 |
Cost drives most readers to a single organization: $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, meaning member and role management has no Management API surface at all; it is
dashboard-only regardless of plan.
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 is per member, not per organization. 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 buy 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. Member management is dashboard-only, so 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 you want to find before the window, not during it. 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 whole loop scripts cleanly:
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. On the cheap scoped read that rate-limits L01
burst, the breach was the API’s own JSON 429 ThrottlerException with
retry-after: 60 at request 118 of a 120 budget (2026-08-17); the HTML page was seen
on aggressive polling of other endpoints, where a client that assumes JSON throws a
parse error instead of seeing something retryable. 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 is unscoped
Section titled “Part 4: the token that does all of this is unscoped”A PAT has no scopes. It reads and writes every organization and every project the account can reach, including creating organizations and deleting projects.
There is no token-scoping surface to reach for. /v1/oauth/apps,
/v1/profile/permissions, /v1/tokens and /v1/profile/access-tokens all return
404, while /v1/organizations and /v1/profile return 200 on the same token. Every
documented endpoint advertises Auth: bearer | fga_permissions, but no
fine-grained-authorization surface is reachable with a PAT.
So consolidating into one organization also consolidates blast radius: one leaked
token now reaches every client rather than one. Mitigations are operational rather
than technical - short-lived tokens, a dedicated automation account, and rotating
after a migration run. The one piece of real per-credential scoping in the platform
is at the data plane, not here: 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 is different, and the cause is not the transfer. A Free organization only offers Nano compute, so 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.
That matters because 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”The practices this runbook carries, collected with the module or run each rests on. 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 PAT is unscoped, so the user’s membership set is the blast radius. | 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 before every move, not just the first. 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”. About 19 seconds of divergence measured between the control plane reporting healthy and the data plane serving.- 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. This bites during post-move config cleanup rather than during 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. Transfers sidestep this entirely, since the config never moves. 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. Another thing a transfer makes moot.- Rate limiting arrives as HTML. Sequential probing earns a Cloudflare interstitial, not a JSON 429. Back off and check the content type.
- 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 |
| A PAT is unscoped | Probed every plausible scope/permission endpoint against a working token | Measured - all 404 while normal endpoints return 200 |
| 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 - role assignment has no API surface | 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 |
| S06 | sfp-platforms | s06-platform-entitlements.ts | none published |
| S07 | sfp-platforms | s07-read-replicas.ts | none published |
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 ↩