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
Row-Level Security (Postgres)
Schema-Per-Tenant (Postgres)
ORM with Tenant Filtering (Prisma)
-- 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 dat a
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
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
Scope All Queries to Organization
Include organizationId in every database query: const data = await db . data . findMany ({
where: {
organizationId: req . organization . id ,
// other filters
},
});
Use External IDs for Integration
Link WorkOS organizations to your existing tenant IDs: await workos . organizations . createOrganization ({
name ,
externalId: yourInternalTenantId ,
});
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 },
});
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