Skip to content

How To Use Organizations And Memberships

Use this guide when your app needs team, workspace, tenant, or customer-account boundaries.

Seamless Auth organizations give you:

  • organization records
  • membership records
  • organization membership roles and scopes
  • an active organization pointer on the current session
  • admin routes for operator-level organization management

They do not replace app-specific authorization for your product data. Your backend still needs to check whether the authenticated user can read or mutate each app resource.


User
-> OrganizationMembership
-> Organization
Session
-> active organizationId

An organization has:

FieldMeaning
idOrganization UUID
nameDisplay name
slugUnique slug
createdByUserIdUser that created the organization
metadataOptional caller-defined object

A membership has:

FieldMeaning
organizationIdOwning organization
userIdMember user
rolesOrganization-level roles such as owner, admin, member
scopesOrganization-level scopes such as members:read

When a user creates an organization, Seamless Auth gives that user membership roles owner and admin, with the scopes organization:read, organization:write, members:read, and members:write.


Through the Express adapter, browser apps call these routes at /auth/organizations/*.

MethodRoutePurpose
GET/auth/organizationsList current user’s organizations
POST/auth/organizationsCreate an organization
GET/auth/organizations/:organizationIdGet one organization
PATCH/auth/organizations/:organizationIdUpdate an organization
POST/auth/organizations/:organizationId/switchSet active organization for the current session
GET/auth/organizations/:organizationId/membersList members
POST/auth/organizations/:organizationId/membersAdd a member
PATCH/auth/organizations/:organizationId/members/:userIdUpdate member roles and scopes
DELETE/auth/organizations/:organizationId/members/:userIdRemove a member

Admin routes are also exposed under /auth/admin/organizations/* and require scoped admin access in the auth API.


AuthProvider keeps the current user, organizations, and active organization in context.

import { useAuth } from '@seamless-auth/react'
export function CurrentOrganizationBadge() {
const { activeOrganization } = useAuth()
if (!activeOrganization) {
return <span>No organization selected</span>
}
return <span>{activeOrganization.name}</span>
}

The provider refreshes this state after sign-in and after switchOrganization(...).


Use the headless client for create/update/member operations.

import { useState } from 'react'
import { useAuth, useAuthClient, type OrganizationResult } from '@seamless-auth/react'
export function CreateOrganizationForm() {
const authClient = useAuthClient()
const { refreshSession } = useAuth()
const [name, setName] = useState('')
async function submit(event: React.FormEvent) {
event.preventDefault()
const response = await authClient.createOrganization({
name,
metadata: { source: 'self-service' },
})
if (!response.ok) {
throw new Error('Could not create organization')
}
const result = (await response.json()) as OrganizationResult
await authClient.switchOrganization(result.organization.id)
await refreshSession()
}
return (
<form onSubmit={submit}>
<label htmlFor="organizationName">Organization name</label>
<input id="organizationName" value={name} onChange={(event) => setName(event.target.value)} />
<button type="submit">Create</button>
</form>
)
}

If you omit slug, the auth API derives one from the name and makes it unique.


For basic switching, use the provider helper:

import { useAuth } from '@seamless-auth/react'
export function OrganizationSwitcher() {
const { organizations, activeOrganization, switchOrganization } = useAuth()
return (
<select
value={activeOrganization?.id ?? ''}
onChange={(event) => void switchOrganization(event.target.value)}
>
<option value="" disabled>
Select organization
</option>
{organizations.map((organization) => (
<option key={organization.id} value={organization.id}>
{organization.name}
</option>
))}
</select>
)
}

Switching updates the server-side session organizationId and causes the adapter to set a fresh access cookie.


import { useEffect, useState } from 'react'
import {
useAuth,
useAuthClient,
type OrganizationMembership,
type OrganizationMembersResult,
} from '@seamless-auth/react'
export function MemberList() {
const { activeOrganization } = useAuth()
const authClient = useAuthClient()
const [members, setMembers] = useState<OrganizationMembership[]>([])
useEffect(() => {
if (!activeOrganization) return
async function load() {
const response = await authClient.listOrganizationMembers(activeOrganization.id)
if (!response.ok) {
setMembers([])
return
}
const result = (await response.json()) as OrganizationMembersResult
setMembers(result.members)
}
void load()
}, [activeOrganization, authClient])
return (
<ul>
{members.map((member) => (
<li key={member.id}>
{member.user?.email ?? member.userId}: {member.roles.join(', ')}
</li>
))}
</ul>
)
}

Members can be listed by users with members:read, organization managers, or global admin read access.


Add a member by userId or email.

await authClient.addOrganizationMember(activeOrganization.id, {
email: 'teammate@example.com',
roles: ['member'],
scopes: ['organization:read'],
})

Update membership roles or scopes:

await authClient.updateOrganizationMember(activeOrganization.id, userId, {
roles: ['admin'],
scopes: ['organization:read', 'organization:write', 'members:read', 'members:write'],
})

Remove a member:

await authClient.removeOrganizationMember(activeOrganization.id, userId)

The auth API protects owner removal: an organization must keep at least one owner.


Step 6: Protect Product Data On Your Backend

Section titled “Step 6: Protect Product Data On Your Backend”

Organization membership is not the same thing as app data authorization.

Use Seamless Auth for organization identity and membership state, then enforce your product’s resource rules on the backend.

import { requireAuth } from '@seamless-auth/express'
app.get(
'/api/organizations/:organizationId/projects',
requireAuth({ cookieSecret }),
async (req, res) => {
const allowed = await canReadProjects({
userId: req.user.id,
organizationId: req.params.organizationId,
})
if (!allowed) {
res.status(403).json({ error: 'Insufficient organization access' })
return
}
res.json({
projects: await listProjects(req.params.organizationId),
})
},
)

Prefer route parameters or explicit request data for app resources. Do not rely on hiding an organization selector in the UI as your tenant boundary.


Seamless Auth has two role concepts:

ConceptStored onUse for
Global user rolesusers.rolesApp-wide access such as admin:read
Organization membership roles/scopesorganization_membershipsTenant-level membership and organization management

Membership roles are intentionally simple. A pragmatic default is:

Membership roleUse for
ownerBilling, ownership, destructive tenant actions
adminOrganization management
memberNormal team access

Scopes should be concrete and action-oriented:

  • organization:read
  • organization:write
  • members:read
  • members:write
  • app-specific scopes such as projects:read or projects:write

MistakeFix
Treating organization membership as automatic access to app dataAdd backend checks for each app resource
Using global admin for tenant actionsUse membership roles/scopes for tenant-level access
Removing the last ownerKeep at least one owner membership
Forgetting to refresh after create/switchCall switchOrganization(...) or refreshSession()
Assuming active organization is always setHandle users with no organizations or no selected organization