Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/workos/workos-node/llms.txt

Use this file to discover all available pages before exploring further.

The WorkOS Node SDK provides comprehensive organization management capabilities for building B2B SaaS applications with proper tenant isolation, role-based access control, and enterprise features.

Core Concepts

Organizations

Organizations represent tenant accounts in your application:
interface Organization {
  id: string;                              // org_123
  name: string;                            // Acme Corp
  allowProfilesOutsideOrganization: boolean;
  domains: OrganizationDomain[];           // Verified domains
  createdAt: string;
  updatedAt: string;
  externalId: string | null;               // Your internal ID
  metadata: Record<string, string>;        // Custom data
}

Organization Memberships

Users belong to organizations through memberships, which define their role and permissions.

Organization Management

Creating Organizations

import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS('sk_...');

const organization = await workos.organizations.createOrganization({
  name: 'Acme Corporation',
  domainData: [
    {
      domain: 'acme.com',
      state: 'verified',
    },
  ],
  externalId: 'acme_corp_123', // Your internal tenant ID
  metadata: {
    plan: 'enterprise',
    industry: 'technology',
    employeeCount: '500',
  },
});
Use externalId to link WorkOS organizations with your internal tenant IDs for easy lookups.

Self-Service Organization Creation

Let users create their own organizations during signup:
app.post('/api/signup', async (req, res) => {
  const { email, password, companyName } = req.body;
  
  try {
    // Create organization first
    const organization = await workos.organizations.createOrganization(
      {
        name: companyName,
        metadata: {
          plan: 'free',
          createdVia: 'self_signup',
        },
      },
      {
        idempotencyKey: `org-${email}-${Date.now()}`,
      }
    );
    
    // Create user
    const user = await workos.userManagement.createUser({
      email,
      password,
    });
    
    // Add user to organization as owner
    await workos.userManagement.createOrganizationMembership({
      userId: user.id,
      organizationId: organization.id,
      roleSlug: 'owner',
    });
    
    res.json({ organization, user });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Listing and Searching Organizations

// List all organizations
const organizations = await workos.organizations.listOrganizations();

for await (const org of organizations) {
  console.log(org.name, org.id);
}

// Filter by domain
const acmeOrgs = await workos.organizations.listOrganizations({
  domains: ['acme.com'],
});

// Pagination
const firstPage = await workos.organizations.listOrganizations({
  limit: 10,
});

for await (const org of firstPage) {
  console.log(org.name);
}

Updating Organizations

const updated = await workos.organizations.updateOrganization({
  organization: 'org_123',
  name: 'Acme Corporation Inc.',
  domainData: [
    {
      domain: 'acme.com',
      state: 'verified',
    },
    {
      domain: 'acmecorp.com',
      state: 'verified',
    },
  ],
  metadata: {
    plan: 'enterprise',
    seats: '50',
  },
});

Retrieving Organizations

// By WorkOS ID
const org = await workos.organizations.getOrganization('org_123');

// By your external ID
const org = await workos.organizations.getOrganizationByExternalId(
  'acme_corp_123'
);

Tenant Isolation

Request-Scoped Middleware

Ensure every request is scoped to a single organization:
import { Request, Response, NextFunction } from 'express';

async function requireOrganization(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const organizationId = req.headers['x-organization-id'] as string;
  
  if (!organizationId) {
    return res.status(400).json({ 
      error: 'Missing organization context',
    });
  }
  
  try {
    // Verify organization exists and user has access
    const memberships = await workos.userManagement.listOrganizationMemberships({
      userId: req.user.id,
      organizationId,
    });
    
    let hasMembership = false;
    for await (const membership of memberships) {
      if (membership.organizationId === organizationId) {
        hasMembership = true;
        req.membership = membership;
        break;
      }
    }
    
    if (!hasMembership) {
      return res.status(403).json({ 
        error: 'Not a member of this organization',
      });
    }
    
    // Fetch organization details
    req.organization = await workos.organizations.getOrganization(
      organizationId
    );
    
    next();
  } catch (error) {
    res.status(500).json({ error: 'Failed to verify organization access' });
  }
}

// Usage
app.get(
  '/api/data',
  requireAuth(),
  requireOrganization,
  async (req, res) => {
    // All queries scoped to req.organization.id
    const data = await db.data.findMany({
      where: { organizationId: req.organization.id },
    });
    
    res.json(data);
  }
);

Database-Level Isolation

-- Create organizations table
CREATE TABLE organizations (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- All tenant data includes organization_id
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  organization_id TEXT NOT NULL REFERENCES organizations(id),
  title TEXT NOT NULL,
  content TEXT
);

-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only see their org's data
CREATE POLICY organization_isolation ON documents
  FOR ALL
  USING (organization_id = current_setting('app.current_organization_id')::TEXT);

// Set organization context in each request
app.use(async (req, res, next) => {
  if (req.organization) {
    await db.query(
      "SET LOCAL app.current_organization_id = $1",
      [req.organization.id]
    );
  }
  next();
});

Multi-Organization Users

Many B2B apps allow users to belong to multiple organizations:

Organization Switcher

app.get('/api/user/organizations', requireAuth(), async (req, res) => {
  const memberships = await workos.userManagement.listOrganizationMemberships({
    userId: req.user.id,
  });
  
  const organizations = [];
  for await (const membership of memberships) {
    const org = await workos.organizations.getOrganization(
      membership.organizationId
    );
    
    organizations.push({
      id: org.id,
      name: org.name,
      role: membership.roleSlug,
      status: membership.status,
    });
  }
  
  res.json({ organizations });
});

Switch Active Organization

app.post('/api/user/switch-organization', requireAuth(), async (req, res) => {
  const { organizationId } = req.body;
  
  // Verify user is a member
  const memberships = await workos.userManagement.listOrganizationMemberships({
    userId: req.user.id,
    organizationId,
  });
  
  let hasMembership = false;
  for await (const membership of memberships) {
    if (membership.organizationId === organizationId) {
      hasMembership = true;
      break;
    }
  }
  
  if (!hasMembership) {
    return res.status(403).json({ error: 'Not a member' });
  }
  
  // Refresh session with new organization context
  const session = workos.userManagement.loadSealedSession({
    sessionData: req.cookies['wos-session'],
    cookiePassword: process.env.WORKOS_COOKIE_PASSWORD!,
  });
  
  const refreshed = await session.refresh({
    organizationId,
  });
  
  if (!refreshed.authenticated) {
    return res.status(401).json({ error: 'Session refresh failed' });
  }
  
  res.cookie('wos-session', refreshed.sealedSession, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
  });
  
  res.json({ success: true, organizationId });
});

Role-Based Access Control

List Organization Roles

const roles = await workos.organizations.listOrganizationRoles({
  organizationId: 'org_123',
});

console.log(roles.data); // [{ slug: 'owner', name: 'Owner' }, ...]

Check User Role

function requireRole(...allowedRoles: string[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    if (!req.membership) {
      return res.status(403).json({ error: 'No organization membership' });
    }
    
    if (!allowedRoles.includes(req.membership.roleSlug)) {
      return res.status(403).json({ 
        error: `Requires one of: ${allowedRoles.join(', ')}`,
      });
    }
    
    next();
  };
}

app.delete(
  '/api/organizations/:orgId/users/:userId',
  requireAuth(),
  requireOrganization,
  requireRole('admin', 'owner'),
  async (req, res) => {
    // Only admins and owners can delete users
    await workos.userManagement.deleteUser(req.params.userId);
    res.json({ success: true });
  }
);

Managing Memberships

// Add user to organization
const membership = await workos.userManagement.createOrganizationMembership({
  userId: 'user_123',
  organizationId: 'org_456',
  roleSlug: 'member',
});

// Update role
const updated = await workos.userManagement.updateOrganizationMembership(
  membership.id,
  {
    roleSlug: 'admin',
  }
);

// Deactivate (soft delete)
const deactivated = await workos.userManagement.deactivateOrganizationMembership(
  membership.id
);

// Reactivate
const reactivated = await workos.userManagement.reactivateOrganizationMembership(
  membership.id
);

// Permanently delete
await workos.userManagement.deleteOrganizationMembership(membership.id);

Invitations

Invite users to join organizations:
app.post('/api/invitations', requireAuth(), requireRole('admin', 'owner'), async (req, res) => {
  const { email, roleSlug } = req.body;
  
  const invitation = await workos.userManagement.sendInvitation({
    email,
    organizationId: req.organization.id,
    inviterUserId: req.user.id,
    roleSlug,
    expiresInDays: 7,
  });
  
  res.json({ invitation });
});

// List pending invitations
app.get('/api/invitations', requireAuth(), async (req, res) => {
  const invitations = await workos.userManagement.listInvitations({
    organizationId: req.organization.id,
  });
  
  const pending = [];
  for await (const invitation of invitations) {
    if (invitation.state === 'pending') {
      pending.push(invitation);
    }
  }
  
  res.json({ invitations: pending });
});

// Revoke invitation
app.delete('/api/invitations/:id', requireAuth(), async (req, res) => {
  await workos.userManagement.revokeInvitation(req.params.id);
  res.json({ success: true });
});

Feature Flags

Manage per-organization feature access:
// List organization features
const features = await workos.organizations.listOrganizationFeatureFlags({
  organizationId: 'org_123',
});

for await (const feature of features) {
  console.log(feature.key, feature.enabled);
}

// Check feature access
async function hasFeature(
  organizationId: string,
  featureKey: string
): Promise<boolean> {
  const features = await workos.organizations.listOrganizationFeatureFlags({
    organizationId,
  });
  
  for await (const feature of features) {
    if (feature.key === featureKey) {
      return feature.enabled;
    }
  }
  
  return false;
}

// Feature gate middleware
function requireFeature(featureKey: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const enabled = await hasFeature(req.organization.id, featureKey);
    
    if (!enabled) {
      return res.status(403).json({ 
        error: `Feature '${featureKey}' not available for this organization`,
      });
    }
    
    next();
  };
}

app.get(
  '/api/advanced-analytics',
  requireAuth(),
  requireOrganization,
  requireFeature('advanced_analytics'),
  async (req, res) => {
    // Only available to orgs with advanced_analytics enabled
    res.json({ analytics: [] });
  }
);

Organization API Keys

Provide organization-scoped API keys for customers:
// Create org API key
const apiKey = await workos.organizations.createOrganizationApiKey(
  {
    organizationId: 'org_123',
    name: 'Production API Key',
  },
  {
    idempotencyKey: `api-key-${Date.now()}`,
  }
);

console.log(apiKey.secret); // Only shown once!

// List API keys
const keys = await workos.organizations.listOrganizationApiKeys({
  organizationId: 'org_123',
});

for await (const key of keys) {
  console.log(key.name, key.createdAt);
}

Best Practices

1

Always Validate Organization Access

Never trust organization IDs from client requests. Always verify the user has access:
const memberships = await workos.userManagement.listOrganizationMemberships({
  userId: req.user.id,
  organizationId: req.body.organizationId,
});
// Verify membership exists
2

Scope All Queries to Organization

Include organizationId in every database query:
const data = await db.data.findMany({
  where: { 
    organizationId: req.organization.id,
    // other filters
  },
});
3

Use External IDs for Integration

Link WorkOS organizations to your existing tenant IDs:
await workos.organizations.createOrganization({
  name,
  externalId: yourInternalTenantId,
});
4

Implement Organization Switching

For users in multiple orgs, provide a clear organization context:
// Include current org in every response
res.json({
  data: results,
  organization: { id: req.organization.id, name: req.organization.name },
});
5

Track Organization in Audit Logs

All audit logs should include organization context:
await workos.auditLogs.createEvent(req.organization.id, event);

Complete Example: Multi-Tenant API

import express from 'express';
import { WorkOS } from '@workos-inc/node';

const app = express();
const workos = new WorkOS('sk_...');

// Middleware: Load organization context
app.use(async (req, res, next) => {
  const orgId = req.headers['x-organization-id'];
  if (!orgId) return next();
  
  try {
    req.organization = await workos.organizations.getOrganization(
      orgId as string
    );
  } catch {}
  
  next();
});

// Organization routes
app.post('/api/organizations', requireAuth(), async (req, res) => {
  const org = await workos.organizations.createOrganization({
    name: req.body.name,
    externalId: generateTenantId(),
  });
  
  await workos.userManagement.createOrganizationMembership({
    userId: req.user.id,
    organizationId: org.id,
    roleSlug: 'owner',
  });
  
  res.json({ organization: org });
});

// Tenant-scoped data access
app.get('/api/documents', requireAuth(), requireOrganization, async (req, res) => {
  const documents = await db.document.findMany({
    where: { organizationId: req.organization.id },
  });
  
  res.json({ documents });
});

// Admin-only endpoints
app.post(
  '/api/invitations',
  requireAuth(),
  requireOrganization,
  requireRole('admin', 'owner'),
  async (req, res) => {
    const invitation = await workos.userManagement.sendInvitation({
      email: req.body.email,
      organizationId: req.organization.id,
      inviterUserId: req.user.id,
    });
    
    res.json({ invitation });
  }
);

app.listen(3000);

API Reference

See the source code for implementation details:
  • createOrganization() - src/organizations/organizations.ts:66
  • listOrganizations() - src/organizations/organizations.ts:45
  • getOrganization() - src/organizations/organizations.ts:83
  • updateOrganization() - src/organizations/organizations.ts:99
  • listOrganizationMemberships() - src/user-management/user-management.ts:906