Users in multiple organizations: the data model and token design

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/dashboard vs /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.

If your product allows users to see content from multiple orgs in a unified view (e.g., a dashboard showing all tasks across all orgs), the access token must be scoped appropriately. Either issue a multi-org token with explicit per-org permissions (complex but unified), or make separate API calls per org and merge results client-side (simpler, more auditable). Most products choose the latter — the frontend aggregates multiple org-scoped responses.

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.

← Back to blog Try Bastionary free →