Skip to content

Consolidating Supabase accounts into 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.

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.

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.

Fixed facts every later step depends on.

FactValue
Transfer endpoint pairPOST /v1/projects/{ref}/claim-token, then POST /v1/organizations/{slug}/project-claim/{token}
Dry-run endpointGET /v1/organizations/{slug}/project-claim/{token}
Claim token lifetime1 hour
Measured transfer time2.3 s paid to paid, 4.0 s paid to Free
Free Plan project cap2 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 planMicro - Nano is refused at creation
Management API basehttps://api.supabase.com/v1

Everything below assumes a Personal Access Token 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 levelConsequence
Billing and invoicingOne invoice covering every client
Usage quotasEgress, storage and MAU pool across all projects, so one client’s spike consumes everyone’s allowance
Fair-use restrictionsApplied to the organization, so an overdue invoice affects every project in it
Member rolesOwner and Administrator reach all current and future projects
PlanOne plan per organization; projects cannot be on different plans within it

That last pair is the decision. 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 situationTopology
Clients never touch the dashboard; you operate everything and bill centrallyOne organization, one project per client
Clients need dashboard access scoped to their own projectTeam plan (project-scoped roles) or one organization per client
Clients need separate invoicesOne organization per client
A client may take ownership of their project laterOne organization per client - though a transfer makes this a one-command change either way
Different clients need different plansOne 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.

Rather than trusting a pricing page, read the entitlements for an organization directly:

Terminal window
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/organizations/<slug>/entitlements" | jq

Differences that affect this decision, measured on a Pro and a Team organization:

EntitlementProTeam
project_scoped_rolesfalsetrue
security.member_rolesOwner, Administrator, Developerplus Read-only
security.audit_logs_days062
log.retention_days728
backup.retention_days714
auth.platform.ssofalsetrue
audit_log_drainsfalsetrue
function.max_count5001000
project_pausingfalsefalse
api.members.rolesfalsefalse

Two rows matter beyond the feature comparison. project_pausing is false on both paid plans - a paid project cannot be paused, so an idle client project bills until you delete it or move it to a Free organization. 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 number
of active free projects within organizations where they are an administrator or
owner: <member> (2 project limit). To continue, these users will need to either
delete, 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.

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.

Terminal window
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.

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.

Terminal window
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 worth recognizing:

KeyMeaning
WARN_COMPUTE_COSTS_INCREASE_IN_TARGET_ORGANIZATIONThe destination already has projects, so its compute credit is spent; this project adds about $10/month
WARN_TARGET_ORGANIZATION_NOT_ON_USAGE_BILLINGDestination is not on usage-based billing
WARN_TARGET_ORGANIZATION_ON_FREE_PLAN_AND_PROJECT_ON_HIGHER_COMPUTEThe instance will be downgraded to Nano, with downtime - see Part 4
INFO_NO_AUTOMATIC_COMPUTE_UPGRADELanding in a paid organization entitles you to a free Micro upgrade, but you have to trigger it
ERR_TARGET_ORGANIZATION_EXCEEDS_FREE_PROJECT_LIMITvalid 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:

Terminal window
curl -s -X DELETE -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$REF/claim-token"

Same token, same path, POST:

Terminal window
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:

Terminal window
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:

Terminal window
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 15
done

The sleep is rate-limit courtesy. The Management API throttles aggressive polling and answers with a Cloudflare HTML interstitial rather than a JSON 429, so a client that assumes JSON throws a parse error instead of seeing something retryable.

The token that does all of this is unscoped

Section titled “The token that does all of this is unscoped”

Worth being explicit about before you point a script at every client project: a Personal Access Token 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. That is untested here and is not a substitute for Management API scoping.

Part 4: the one direction that costs you downtime

Section titled “Part 4: 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:

DirectionMeasured data-plane outage
Team to Pronone above the noise floor
Pro to Teamnone above the noise floor
Free to Pronone above the noise floor
Paid to Free75.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.

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 to scale-to-zero is granted per account rather than being generally available. If your reason for consolidating is the cost of idle client projects, that is worth asking about before you optimize around it; we could not test it on this account.

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:

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. Check what a project is currently on:

Terminal window
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.

CheckHowExpected
Project movedGET /v1/projects/{ref}organization_id is the destination
Ref unchangedcompare before and afteridentical - no client repointing
Keys unchangedGET /v1/projects/{ref}/api-keysall four keys identical (legacy anon, legacy service_role, publishable, secret)
Signing keys unchangedGET /v1/projects/{ref}/config/auth/signing-keyssame key ids and algorithms
Auth config unchangedGET /v1/projects/{ref}/config/authidentical
Data intactselect count(*) on a known tableunchanged
Storage intactfetch a known object through the Storage APIsame bytes
Edge Functions intactinvoke a deployed functionsame response
Vault intactselect decrypted_secret from vault.decrypted_secretsplaintext, no error
Data plane healthyGET https://<ref>.supabase.co/auth/v1/health200 - poll this, not project status
Compute as intendedGET /v1/projects/{ref}/billing/addonsMicro add-on present if you took the free upgrade
Source organization emptyGET /v1/projectsno projects left under the old organization

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.

PreservedNot preserved
Project ref and URLOrganization membership (the point of the exercise)
All four API keysYour effective rights, which follow your role in the destination
Signing key ids and algorithms
pgsodium encryption root key
Auth configuration
PostgREST configuration
Database rows and roles
Storage buckets and object bodies
Edge Function bodies and secrets
Region (it cannot be changed)

This is the substantive 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.

  • /v1/projects/{ref}/transfer returns 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_HEALTHY is not “ready”. About 19 seconds of divergence measured between the control plane reporting healthy and the data plane serving.
  • A partial SMTP PATCH wipes SMTP. Setting the full block and later patching smtp_pass alone left smtp_host, smtp_user and smtp_pass all 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/auth are 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 configuration never moves.
  • jwt_secret is not exposed by GET /v1/projects/{ref}/config/auth. New projects sign with an ES256 key with the legacy HS256 secret demoted to previously_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 /v1 anyway. 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 /v1 but not deleted there. Cleanup of an emptied source organization is a dashboard action.

Measured 2026-08-03 unless marked otherwise.

ClaimHow it was checkedResult
Transfer runs on the public Management APIExecuted the claim-token pair with a PAT, no dashboard involvedMeasured - 2.3 s paid to paid
Transfer preserves ref, keys, signing keys, root key, config, data, storage, functions15-field fingerprint diffed pre/post across a Team-Pro-Team round tripMeasured - only organization_id changed
Paid-to-paid transfer causes no outagePolled /auth/v1/health at 250 ms across three movesMeasured - nothing above the host’s ambient failure floor
Paid-to-Free costs about 75 sSame probe across the moveMeasured - 75.2 s continuous, HTTP 521/525
Free Plan cap is per member across organizationsCreated free projects until refusedMeasured - verbatim API error
The cap is enforced at transfer time tooPreviewed a third project into a Free organization at the capMeasured - valid: false, ERR_TARGET_ORGANIZATION_EXCEEDS_FREE_PROJECT_LIMIT
Nano is refused at creation on a paid planAttempted itMeasured - 400 Minimum instance size on paid plans is Micro
A transferred-in Free project keeps NanoCompared billing/addons against a Micro projectMeasured - no compute add-on selected
Nano in a paid organization bills at the Micro rateRead the preview warningMeasured - “will increase by $10/month”
Micro is $0.01344/hourGET /v1/projects/{ref}/billing/addonsMeasured
Plan matrix (project-scoped roles, Read-only, pausing, API member management)GET /v1/organizations/{slug}/entitlements on a Pro and a Team organizationMeasured
A Personal Access Token is unscopedProbed every plausible scope/permission endpoint against a working tokenMeasured - all 404 while normal endpoints return 200
Per-service health is a separate endpointGET /v1/projects/{ref}/health?services=... on a live projectMeasured - four services reporting independently
Control plane leads the data plane by ~19sCompared aggregate project status against the data planeObserved, method superseded - not re-measured against the per-service endpoint
Scale-to-zero on NanoPlatform documentation; gated per accountDocumented, not tested
Secret keys can be bound to a role via secret_jwt_templatePlatform documentationDocumented, not tested
Fine-grained authorization (fga_permissions)Advertised on every endpoint; no surface reachable with a PATNot tested
Credential fields return a project-scoped digestWrote the same secret to two projects and comparedMeasured
Partial SMTP PATCH clears SMTPSet the full block, patched one field, re-readMeasured
A GitHub integration blocks transferNot run - creating the connection needs the dashboard OAuth flowDocumented, not tested
A project-scoped role blocks transferNot run - role assignment has no API surfaceDocumented, not tested
Quotas pool organization-wideNot run - usage endpoints are not on /v1Documented, not tested
Fair-use restriction covers all projects in an organizationNot run - deliberately, tripping fair use on a live account is out of scopeDocumented, not tested
An organization-scoped role covers future projectsNot run - needs dashboard member managementDocumented, not tested
Rights follow your role in the destination organizationNot run - needs a second accountDocumented, not tested
Source organization is billed up to the transfer, destination afterNot run - needs two full billing cyclesDocumented, not tested