.do
Enterprises

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: abc123xyz789

2. 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 context

2. 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 }
  })
})