Slices
Organizations
Tenants, roles, invitations and the guards that keep one tenant's rows out of another's.The orgs slice makes an app multi-tenant: every user belongs to at least
one organization, and org-scoped pages live under /app/[org]. It requires
auth and adds no dependency of its own. The dashboard surface requires
it; name it only for a composition without the dashboard:
Run supabase/orgs.sql after init.sql. It creates organizations,
organization_members and organization_invitations.
Last updated October 18, 2026/app resolves the caller's default org and redirects
/app/acme the org's home
/app/acme/settings about the tenant
/profile about the person, deliberately outside /app
The tenant sits under /app/ rather than at the root because a composed
app shares its root with other surfaces: an organization slugged store
would be shadowed by the store surface. Slugs are lowercase letters, digits
and hyphens, and never change once created.
/app creates the caller's personal organization on the first
authenticated request that needs one, never by a trigger, then redirects.
Every user always has one, so no row is ever tenant-less.
role is a column on organization_members: owner, admin or
member. An organization always keeps at least one owner, only an owner
can make someone an owner or act on another owner, and ownership changes
hands in two steps (promote, then step down). A personal organization
cannot be left, deleted or invited into.
RLS is enabled with no policies and the server acts through the
service-role client, so nothing in the database scopes a row to a
tenant. The guards in src/lib/orgs/server.ts are the only boundary.
They return a result rather than throwing:
- requireOrg(slug) — any member.
- requireOrgRole(slug, role) — a member with at least role.
- requireOrgAdmin(slug) — owner or admin.
- requireOrgOwner(slug) — owner.
- requireOrgResource(table, id, options) — a member of whichever org owns that row.
- GET/POST /api/orgs — signed in: list (creating the personal org) or create.
- GET/PATCH/DELETE /api/orgs/[org] — requireOrg / requireOrgAdmin / requireOrgOwner.
- GET /api/orgs/[org]/members — requireOrg, one server-side page.
- PATCH/DELETE /api/orgs/[org]/members — requireOrgAdmin; DELETE ?userId=me leaves.
- GET/POST/DELETE /api/orgs/[org]/invitations — requireOrgAdmin.
- GET/POST /api/invitations/[token] — signed in: preview and accept.
Edit this page on GitHub