Organizations
Manage multi-tenant enterprise organizations with WorkOS, including domain verification, member management, and hierarchical structures
Organizations
Organizations in the .do platform represent enterprise customers, each with their own isolated data, users, SSO connections, and directory sync configurations. WorkOS Organizations provides the foundation for building secure multi-tenant B2B applications.
Overview
Organizations enable:
- Multi-Tenancy - Isolated environments for each customer
- Domain Verification - Prove ownership of company domains
- SSO & Directory Sync - Per-organization authentication configuration
- Member Management - Manage users within organizations
- Hierarchical Structures - Support for departments and teams
- Custom Branding - Per-organization UI customization
- Usage Tracking - Monitor resource consumption per organization
Organization Data Model
{
id: 'org_01HXYZ...',
name: 'Acme Corporation',
domains: [
{
id: 'org_domain_01ABC...',
domain: 'acme.com',
state: 'verified'
},
{
id: 'org_domain_01DEF...',
domain: 'acme.co.uk',
state: 'pending'
}
],
createdAt: '2025-01-15T10:00:00.000Z',
updatedAt: '2025-01-20T14:30:00.000Z'
}Creating Organizations
Via API
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
// Create new organization
const organization = await workos.organizations.createOrganization({
name: 'Acme Corporation',
domains: ['acme.com'],
domainData: [
{
domain: 'acme.com',
state: 'verified' // or 'pending'
}
]
})
console.log(organization)
// {
// id: 'org_01HXYZ...',
// name: 'Acme Corporation',
// domains: [...]
// }During User Signup
Automatically create organizations during signup:
on($.User.signup, async ({ email, name }) => {
const domain = email.split('@')[1]
// Check if organization exists for domain
const existingOrgs = await workos.organizations.listOrganizations({
domains: [domain]
})
let organization
if (existingOrgs.data.length > 0) {
// Join existing organization
organization = existingOrgs.data[0]
} else {
// Create new organization
organization = await workos.organizations.createOrganization({
name: `${domain} Organization`,
domains: [domain],
domainData: [{ domain, state: 'pending' }]
})
// Send domain verification email
await send($.Email.send, {
to: email,
template: 'verify-domain',
data: { organization, domain }
})
}
// Create user in organization
const user = await $.User.create({
email,
name,
organizationId: organization.id,
role: existingOrgs.data.length > 0 ? 'member' : 'admin'
})
return { user, organization }
})Managing Organizations
Update Organization
// Update organization name
await workos.organizations.updateOrganization(
'org_01HXYZ...',
{
name: 'Acme Corp (Updated)'
}
)List Organizations
// List all organizations
const organizations = await workos.organizations.listOrganizations({
limit: 100,
order: 'desc'
})
// List by domain
const orgsByDomain = await workos.organizations.listOrganizations({
domains: ['acme.com']
})
// Pagination
let allOrgs = []
let after = null
while (true) {
const response = await workos.organizations.listOrganizations({
limit: 100,
after
})
allOrgs = allOrgs.concat(response.data)
if (!response.listMetadata.after) break
after = response.listMetadata.after
}Delete Organization
// Delete organization (use with caution!)
await workos.organizations.deleteOrganization('org_01HXYZ...')
// Soft delete (recommended)
await db.update($.Organization, 'org_01HXYZ...', {
state: 'deleted',
deletedAt: new Date()
})Domain Management
Adding Domains
// Add domain to organization
await workos.organizations.updateOrganization(
'org_01HXYZ...',
{
domains: ['acme.com', 'acme.co.uk', 'acme.de']
}
)Domain Verification
Verify domain ownership before allowing SSO:
// Step 1: Initiate verification
const verificationToken = generateToken()
await db.create($.DomainVerification, {
organizationId: 'org_01HXYZ...',
domain: 'acme.com',
verificationToken,
state: 'pending',
method: 'dns', // or 'email' or 'http'
expiresAt: addDays(new Date(), 7)
})
// Step 2: User adds DNS TXT record
// TXT record: _workos-verification=abc123xyz
// Step 3: Verify DNS record
const verified = await verifyDomainDNS('acme.com', verificationToken)
if (verified) {
// Mark domain as verified in WorkOS
await workos.organizations.updateOrganization(
'org_01HXYZ...',
{
domainData: [
{ domain: 'acme.com', state: 'verified' }
]
}
)
await db.update($.DomainVerification, verificationId, {
state: 'verified',
verifiedAt: new Date()
})
}Domain Verification Methods
1. DNS TXT Record (Recommended)
Host: _workos-verification.acme.com
Type: TXT
Value: abc123xyz7892. Email Verification
Send verification email to:
3. HTTP File Verification
Place file at:
Organization Members
Managing Members
// Add user to organization
await $.Organization.addMember({
organizationId: 'org_01HXYZ...',
userId: 'user_01ABC...',
role: 'member' // or 'admin'
})
// List organization members
const members = await db.related(
organization,
$.Organization.has,
$.User,
{ state: 'active' }
)
// Remove member
await $.Organization.removeMember({
organizationId: 'org_01HXYZ...',
userId: 'user_01ABC...'
})
// Transfer ownership
await $.Organization.update('org_01HXYZ...', {
ownerId: 'user_new_owner_01DEF...'
})Member Roles
const roles = {
owner: {
name: 'Owner',
permissions: ['*'] // All permissions
},
admin: {
name: 'Administrator',
permissions: [
'org:manage',
'members:manage',
'billing:manage',
'sso:configure',
'directory:configure',
'settings:update'
]
},
member: {
name: 'Member',
permissions: [
'org:read',
'members:read',
'resources:read',
'resources:write'
]
},
guest: {
name: 'Guest',
permissions: [
'org:read',
'resources:read'
]
}
}
// Check permission
function hasPermission(user, permission) {
const role = roles[user.role]
return role.permissions.includes('*') || role.permissions.includes(permission)
}Organization Hierarchy
Departments & Teams
// Create department
const engineering = await $.Department.create({
name: 'Engineering',
organizationId: 'org_01HXYZ...',
parentId: null // Top-level department
})
// Create sub-department
const backend = await $.Department.create({
name: 'Backend Team',
organizationId: 'org_01HXYZ...',
parentId: engineering.id
})
// Assign user to department
await $.User.assign.Department({
userId: 'user_01ABC...',
departmentId: backend.id
})
// Query hierarchy
const orgStructure = await db.query(`
WITH RECURSIVE dept_tree AS (
-- Root departments
SELECT id, name, parent_id, 0 as level
FROM departments
WHERE organization_id = $1 AND parent_id IS NULL
UNION ALL
-- Child departments
SELECT d.id, d.name, d.parent_id, dt.level + 1
FROM departments d
INNER JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT * FROM dept_tree ORDER BY level, name
`, [organizationId])Multi-Tenancy Isolation
Data Isolation
Ensure all queries are scoped to organization:
// Good: Organization-scoped query
const projects = await db.list($.Project, {
organizationId: user.organizationId
})
// Bad: Global query (security risk!)
const projects = await db.list($.Project)
// Row-level security (PostgreSQL)
await db.execute(`
CREATE POLICY org_isolation ON projects
USING (organization_id = current_setting('app.current_org_id')::uuid)
`)
// Set organization context
await db.execute(`
SET app.current_org_id = '${user.organizationId}'
`)Resource Quotas
Enforce per-organization limits:
const quotas = {
free: {
users: 5,
projects: 3,
storage: 1024 * 1024 * 1024, // 1GB
apiCalls: 1000
},
pro: {
users: 50,
projects: 25,
storage: 100 * 1024 * 1024 * 1024, // 100GB
apiCalls: 100000
},
enterprise: {
users: Infinity,
projects: Infinity,
storage: Infinity,
apiCalls: Infinity
}
}
// Check quota before creating resource
on($.Project.create, async ({ organizationId }) => {
const org = await db.find($.Organization, organizationId)
const quota = quotas[org.plan]
const currentProjects = await db.count($.Project, { organizationId })
if (currentProjects >= quota.projects) {
throw new Error(`Project limit reached (${quota.projects}). Upgrade to create more projects.`)
}
})Organization Settings
Custom Configuration
// Store organization-specific settings
await $.Organization.update(organizationId, {
settings: {
branding: {
logo: 'https://cdn.acme.com/logo.png',
primaryColor: '#FF6B35',
customDomain: 'app.acme.com'
},
features: {
apiAccess: true,
advancedAnalytics: true,
customIntegrations: true
},
security: {
enforceSSO: true,
require2FA: true,
allowedIPs: ['203.0.113.0/24'],
sessionTimeout: 3600
},
notifications: {
email: '[email protected]',
slack: 'https://hooks.slack.com/...',
webhook: 'https://acme.com/webhook'
}
}
})
// Apply organization settings
on($.Request.received, async ({ user, request }) => {
const org = await db.find($.Organization, user.organizationId)
const settings = org.settings
// Enforce SSO
if (settings.security.enforceSSO && user.authMethod !== 'sso') {
throw new Error('SSO authentication required')
}
// IP whitelist
if (settings.security.allowedIPs?.length > 0) {
const ip = request.headers['cf-connecting-ip']
if (!isIPInRanges(ip, settings.security.allowedIPs)) {
throw new Error('IP address not allowed')
}
}
})Organization Billing
Usage Tracking
// Track usage per organization
await send($.Event.log, {
type: 'api.request',
organizationId: user.organizationId,
metadata: {
endpoint: '/api/v1/projects',
method: 'GET',
responseTime: 123
}
})
// Aggregate monthly usage
const usage = await db.query(`
SELECT
organization_id,
COUNT(*) as api_calls,
SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) as successful_calls,
AVG(response_time) as avg_response_time
FROM events
WHERE event_type = 'api.request'
AND timestamp >= date_trunc('month', CURRENT_DATE)
GROUP BY organization_id
`)
// Bill based on usage
for (const org of usage) {
const plan = await db.find($.BillingPlan, { organizationId: org.organization_id })
const cost = calculateCost(plan, org.api_calls)
await $.Invoice.create({
organizationId: org.organization_id,
amount: cost,
period: getCurrentMonth(),
dueDate: addDays(new Date(), 15)
})
}Subscription Management
// Update organization subscription
await $.Organization.update(organizationId, {
subscription: {
plan: 'enterprise',
status: 'active',
startDate: '2025-01-01',
renewalDate: '2026-01-01',
seats: 100,
addOns: ['advanced-analytics', 'priority-support']
}
})
// Check subscription status
function canAccessFeature(organization, feature) {
const plan = plans[organization.subscription.plan]
return plan.features.includes(feature)
}Organization Analytics
Activity Dashboard
// Organization activity metrics
const metrics = await db.query(`
SELECT
date_trunc('day', timestamp) as day,
COUNT(DISTINCT user_id) as active_users,
COUNT(*) FILTER (WHERE event_type = 'api.request') as api_calls,
COUNT(*) FILTER (WHERE event_type = 'user.login') as logins,
COUNT(*) FILTER (WHERE event_type = 'project.created') as projects_created
FROM events
WHERE organization_id = $1
AND timestamp >= NOW() - INTERVAL '30 days'
GROUP BY day
ORDER BY day DESC
`, [organizationId])Migration & Onboarding
Importing Existing Customers
// Bulk import organizations from CSV
const organizations = await parseCsv('customers.csv')
for (const row of organizations) {
const org = await workos.organizations.createOrganization({
name: row.company_name,
domains: [row.domain],
domainData: [{ domain: row.domain, state: 'verified' }]
})
// Migrate users
for (const userEmail of row.user_emails.split(',')) {
await $.User.create({
email: userEmail.trim(),
organizationId: org.id,
role: 'member',
migrated: true
})
}
console.log(`Imported ${row.company_name} (${org.id})`)
}Onboarding Workflow
// Multi-step onboarding
const onboardingSteps = [
{
id: 'create_org',
title: 'Create Organization',
completed: (org) => !!org.id
},
{
id: 'verify_domain',
title: 'Verify Domain',
completed: (org) => org.domains.some(d => d.state === 'verified')
},
{
id: 'configure_sso',
title: 'Set Up SSO',
completed: (org) => org.ssoConnected
},
{
id: 'invite_team',
title: 'Invite Team Members',
completed: async (org) => {
const members = await db.count($.User, { organizationId: org.id })
return members >= 3
}
},
{
id: 'create_project',
title: 'Create First Project',
completed: async (org) => {
const projects = await db.count($.Project, { organizationId: org.id })
return projects >= 1
}
}
]
// Check onboarding progress
const progress = await calculateOnboardingProgress(organizationId, onboardingSteps)
// { completed: 3, total: 5, percentage: 60 }Security Best Practices
1. Organization Context
Always pass organization context:
// Good
const data = await fetchData({ organizationId: user.organizationId })
// Bad
const data = await fetchData() // Missing organization context2. Cross-Organization Access Prevention
// Validate organization ownership
async function validateAccess(userId, resourceId) {
const user = await db.find($.User, userId)
const resource = await db.find($.Resource, resourceId)
if (user.organizationId !== resource.organizationId) {
throw new Error('Access denied: Resource belongs to different organization')
}
return true
}3. Audit Trail
Log all organization changes:
on($.Organization.updated, async ({ organizationId, changes, userId }) => {
await send($.Event.log, {
type: 'org.updated',
organizationId,
userId,
metadata: { changes }
})
})Related Documentation
- Enterprise SSO - Single Sign-On authentication
- Directory Sync - Automatic user provisioning
- Admin Portal - Self-service configuration
- Audit Logs - Compliance and security monitoring
- WorkOS Organizations Docs - Official WorkOS documentation
Directory Sync (SCIM)
Automate user provisioning and deprovisioning with WorkOS Directory Sync supporting SCIM 2.0, Azure AD, Okta, and Google Workspace
Admin Portal
Self-service WorkOS Admin Portal for enterprise IT administrators to configure SSO, Directory Sync, and manage integrations without developer involvement