A freelancer working for three clients, a developer who maintains their own projects alongside their employer's, a consultant whose firm has both internal tooling and customer-facing workspaces — these are the users who belong to multiple organizations. The data model for multi-org membership looks simple until you dig into token design, org switching UX, data isolation requirements, and what happens to a user's content when they leave one org but remain in another.
The data model
The foundational choice: does the user have one identity that belongs to multiple orgs, or separate per-org identities? The shared identity model is simpler and what most SaaS products implement.
-- Shared identity model: one user, multiple org memberships CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE ); CREATE TABLE org_members ( org_id UUID NOT NULL REFERENCES organizations(id), user_id UUID NOT NULL REFERENCES users(id), role TEXT NOT NULL DEFAULT 'member', display_name TEXT, -- org-specific name override (contractor may use different name) joined_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (org_id, user_id) ); -- Track which org the user last worked in (for default org on login) ALTER TABLE users ADD COLUMN last_active_org_id UUID REFERENCES organizations(id);
Session and token design for multi-org users
The critical constraint: access tokens should be scoped to a single organization context. If a user's token contains claims from all their orgs, any downstream service receiving that token needs to understand multi-org context, which creates complexity. A single-org scoped token keeps authorization simple.
// Token design: scoped to the active org
{
"sub": "user_ABC", // stable user identity across all orgs
"org": "org_ACME", // currently active org
"org_slug": "acme", // for display
"org_role": "admin", // role in the active org
"email": "alice@example.com", // user's global email
"exp": 1632134400,
"iat": 1632130800
}
// Org switcher: issue a new token for the selected org
// This is a server-side operation that validates membership
async function switchOrganization(userId: string, targetOrgId: string) {
// Verify the user is actually a member of the target org
const membership = await db.query(
`SELECT role FROM org_members
WHERE user_id = $1 AND org_id = $2`,
[userId, targetOrgId]
);
if (!membership.rows[0]) {
throw new Error('Not a member of this organization');
}
// Update last active org
await db.query(
'UPDATE users SET last_active_org_id = $2 WHERE id = $1',
[userId, targetOrgId]
);
// Issue new token scoped to the new org
return issueAccessToken(userId, targetOrgId, membership.rows[0].role);
}
Org switcher UX considerations
The org switcher is a critical piece of navigation for multi-org users. Key design decisions:
- Where to show it: persistent in the nav, not buried in a settings menu. Users switching between orgs frequently need it to be a single click.
- What to show: org name, the user's role in that org, and a visual indicator of the currently active org.
- On switch: issue a new token, update the URL to reflect the new org context (e.g.,
/acme/dashboardvs/beta/dashboard), preserve the user's current page type if possible (switching orgs while viewing the members page should show the new org's members page). - State isolation: after switching, UI state from the previous org should not leak. Active filters, search terms, and open panels should reset to defaults.
// React: org switcher with token refresh
async function switchOrg(targetOrgId: string) {
// Get new token for the target org
const { accessToken } = await apiClient.post('/auth/switch-org', {
org_id: targetOrgId,
});
// Update token in storage
tokenStore.setAccessToken(accessToken);
// Clear org-scoped state
queryClient.clear(); // clear React Query cache
uiStore.resetOrgState();
// Navigate to the new org context
const currentPath = window.location.pathname;
const newPath = currentPath.replace(/^\/[^/]+/, `/${targetOrgId}`);
router.push(newPath);
}
Personal vs workspace data
When a user belongs to multiple orgs, some data is personal (profile photo, notification preferences, saved searches) and some is org-scoped (projects they created, comments they made, files they uploaded). The distinction matters because:
- Personal data follows the user when they leave an org — it is not the org's data.
- Org-scoped data belongs to the org — a departing user's projects should remain accessible to org members.
- Shared data (like a profile photo that appears in multiple orgs) is stored once against the user record and referenced by org context.
-- Data ownership: user vs org -- User-owned: stays with the user across all orgs CREATE TABLE user_profiles ( user_id UUID PRIMARY KEY REFERENCES users(id), display_name TEXT, avatar_url TEXT, -- user's own avatar, shown in all orgs bio TEXT, timezone TEXT ); -- Org-scoped: belongs to the org, user is the creator CREATE TABLE projects ( id UUID PRIMARY KEY, org_id UUID NOT NULL REFERENCES organizations(id), -- belongs to the org created_by UUID REFERENCES users(id), -- user is the author title TEXT NOT NULL -- When user leaves org: org retains project, created_by is preserved for history );
Consent and data access across orgs
When a user belongs to Org A and Org B, their actions in each org are isolated. Content created in Org A is not visible in Org B. However, there are legitimate use cases where a user might want to share personal data (like a portfolio document) across orgs, or an org might want to access data from a partner org.
Model this as explicit consent grants rather than implicit sharing. The user must actively choose to share their personal data with a specific org, and can revoke that consent at any time. This is similar to OAuth scopes: the user grants a specific permission to a specific organization.
The org membership model is deceptively simple in its basic form but requires careful thought for data isolation, token scoping, and UX. The most common mistake is treating a user's identity and a user's membership as the same thing — they are distinct concepts that happen to be linked, and the distinction matters for deletion, export, and permission checks.