How to Share a Supabase Project: You Share the Organization
There is no per-project invite below the Team plan. The four roles, the 24-hour invite, and why 271 lines of migrations beat a dashboard seat for most collaborators.
You cannot share a single Supabase project on its own. Access is granted at the organization level: you invite someone from the owning organization's team settings, the invite is valid for 24 hours, and the role you give them applies to every project in that organization. Project-scoped roles exist, but only on the Team and Enterprise plans.
That surprises people, and it is usually not what they wanted anyway. "Share the project" turns out to mean four different things, and only one of them is a dashboard seat. This storefront runs a real Supabase project behind its accounts, entitlements and gated downloads, and most of what a new contributor needs is in the repository, not in the dashboard.
Four things "share a Supabase project" can mean
| What you actually want | What to hand over | Where it lives |
|---|---|---|
| A teammate who administers the database | An organization invite with a role | Dashboard → organization team settings |
| A developer who needs to run the app locally | The migrations and .env.example | Your git repository |
| An app or a service that needs to read data | The project URL and the anon key | Environment variables |
| A second project that should carry the same schema | The migrations again, replayed | Your git repository |
| Someone to take the project over permanently | A project transfer | Dashboard → project transfer |
Only the first row is a dashboard invite. The second and fourth are the same answer, which is the most useful thing in this table: for a collaborator, the schema is the artifact, not the seat.
Inviting a person: the four roles
Supabase documents four roles, and the difference between them is mostly about settings, not data:
| Role | What it can do | Caveat |
|---|---|---|
| Owner | Everything, including organization settings, adding other owners, and transferring projects out | At least one owner must always exist |
| Administrator | Everything except updating organization settings, transferring projects out, and adding owners | The default for a trusted engineer |
| Developer | Read-only on organization resources; content access to project resources, but cannot change project settings | The right answer for most invites |
| Read-Only | Read-only on both | Team and Enterprise plans only |
To send one, open the organization's team settings and invite by email. Three details that cost people time:
- The invite expires after 24 hours. A link sent on a Friday is dead by Monday.
- Organization-scoped roles cover current and future projects. Add a Developer today and they have the same access to the project you create next year.
- Project-scoped roles are a paid feature. On Team and Enterprise, a member assigned to specific projects cannot even see the others in the dashboard. On the free plan there is no partial visibility — this is the single biggest reason to put side projects in their own organization rather than piling them into one.
If you would rather check membership from a script than a browser, the Management API answers it directly:
export SUPABASE_ACCESS_TOKEN="your-personal-access-token"
export ORG_ID="your-organization-id"
curl "https://api.supabase.com/v1/organizations/$ORG_ID/members" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
Leaving an organization has one precondition worth knowing before you need it: every organization must keep at least one owner, so a sole owner has to promote someone else, or invite a new owner who accepts, before they can leave.
Sharing the schema instead of a seat
Here is what a new developer on this project actually receives, and it is a directory in git:
supabase/migrations/
0001_commerce_init.sql 78 lines — tables, RLS, read-own-only policies
0002_display_name_allowlist.sql 8
0003_commerce_rpcs.sql 37 — atomic SECURITY DEFINER functions
0004_lock_down_rate_limit_fn.sql 4
0005_retain_anonymized_download_audit.sql 7
0006_pricing_v2_slot_kinds.sql 15
0007_atomic_download_rate_limit.sql 67
0008_refund_requests.sql 55
271 lines, eight files, six row-level-security policies. That is the entire database — schema, constraints, policies and functions — in a form that is reviewable in a pull request, replayable against a brand-new project, and readable by someone who has never logged into your dashboard. The first file opens with the decision it encodes:
-- RLS: read-own only; NO write policies for anon/authenticated — all writes go
-- through server code using the service-role key (which bypasses RLS).
A dashboard invite conveys none of that. A migration file conveys it permanently, to everyone who clones the repository, including the person who inherits it after you leave.
The corollary is a rule rather than a tip: make every schema change as a migration, never in the SQL editor. A change clicked into the dashboard exists in exactly one place and is invisible to code review, to the next environment, and to the collaborator you just invited. Changes made in the editor are also the most common cause of the "it works on my project" class of bug, because the only record of them is the live database.
Sharing connection details
The third row of the table is the one with a trap in it. A Supabase project has two keys, and they are not interchangeable:
# .env.example — committed. .env* is gitignored except this file.
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-or-publishable-key
# SECRET — server-only, bypasses RLS.
SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret
The project URL and the anon key are designed to be public — they ship to the browser on every page load, and row-level security is what makes that safe. Sharing them is not a disclosure; it is the normal mode of operation.
The service-role key is the opposite. It bypasses RLS entirely, which makes it equivalent to handing over the whole database. It belongs in a server environment variable and nowhere else — not in a Slack message, not in a screenshot of the dashboard, and not in a NEXT_PUBLIC_-prefixed variable, which would compile it into the client bundle.
This codebase enforces that mechanically rather than by convention. src/lib/supabase/admin.ts:
import "server-only";
import { createClient as createSbClient } from "@supabase/supabase-js";
/**
* Service-role Supabase client — SERVER ONLY. Bypasses RLS.
* Never import from a Client Component; the `server-only` guard
* makes such an import fail the build.
*/
export function createAdminClient() {
return createSbClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
}
That single import "server-only" line is the difference between a rule in a document and a rule in the build. Any import of this module from a Client Component fails next build with an explicit error. It is the cheapest safety measure in the entire project, and it is the one to copy before you share credentials with anyone.
Two further clients sit beside it — client.ts (browser, anon key) and server.ts (server, anon key, RLS enforced, bound to request cookies). Three files, three trust levels, each one obvious from its name. When a collaborator asks "which client do I use", the answer is a filename.
What to do instead, by situation
| Situation | Do this | Not this |
|---|---|---|
| Contractor working on one feature | Separate organization + its own project, seeded from your migrations | Adding them to the organization holding production |
| Open-source contributor | Repository with supabase/migrations/ + .env.example; they create their own free project | Any credential at all |
| Teammate who needs the dashboard | Organization invite, Developer role | Owner "because it is easier" |
| A second environment (staging) | Replay the same migrations against a new project | pg_dump from production into it |
| Handing the project over for good | Project transfer to their organization | Sharing your account login |
| Agency that needs temporary access | Invite, then remove on the agreed date, then rotate the service-role key | Leaving the invite in place |
That last cell is the step people skip. Removing a member revokes dashboard access; it does not invalidate a service-role key they already copied. If a departing collaborator ever had it, rotate it.
Mistakes and troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "I cannot find a way to invite someone to one project" | Invites are organization-level | Invite to the organization, or move the project to its own organization |
| Invite link does not work | Invites expire after 24 hours | Send a fresh one |
| New member can see every project | They were given an organization-scoped role | Project-scoped roles, on Team/Enterprise — otherwise separate organizations |
| Invite rejected for an SSO organization | Invites from a SAML SSO account can only be accepted by an account from the same identity provider | Invite them through the identity provider |
| Cannot leave the organization | You are the only owner | Promote another member to Owner, or invite one who accepts, then leave |
| Collaborator's local app returns empty tables | They have the credentials but not the schema | supabase/migrations/ in the repository, replayed on their project |
| Queries work locally but 401 in production | Service-role key used locally, anon key in production, with no RLS read policy | Add the read policy; do not promote the service-role key |
| Service-role key ended up in the browser bundle | It was given a NEXT_PUBLIC_ prefix, or imported into a Client Component | Drop the prefix, add import "server-only", and rotate the key |
Frequently asked questions
Can I share one Supabase project without sharing the others? Only with project-scoped roles, which are a Team and Enterprise feature. On the free plan, a member of the organization sees every project in it — so the practical answer is to give each project that needs separate access its own organization.
Is it safe to share the Supabase URL and anon key? Yes, by design — both are shipped to every browser that loads your app. What makes that safe is row-level security on your tables. If a table has RLS off, the anon key is a public read of that table, so audit your policies before you treat the key as harmless.
How do I give someone the database without giving them my account? Commit your migrations. A collaborator creates their own free project and replays them. They get the full schema, constraints and policies with none of your data and none of your credentials.
Does removing a member revoke their API keys? No. Removing a member removes dashboard access. Any API key they copied — especially the service-role key — keeps working until you rotate it.
Templates in this post
Every template below is a static Next.js build with no backend of its own, which is exactly the point: you add Supabase to it with the three-client pattern above, and the marketing pages do not change. All three have a feature that wants a database the moment you launch. ASoc Fade is a barbershop site with services, a price list, team profiles and booking. ASoc Fiscal is a financial-platform marketing site covering payments and invoicing. ASoc Flow is a workflow-automation SaaS landing page with a hub preview and a visual flow builder.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
