.do
Enterprise

Automatic User Provisioning

Real-time user and group synchronization from enterprise directories via SCIM - no manual onboarding or offboarding required

The .do platform automatically synchronizes users from your customers' identity directories. When employees join, leave, or change roles in their company, those changes are reflected in your application immediately - no manual work required.

How It Works

Directory synchronization is automatic when enabled:

// That's it - user provisioning is automatic
on($.Directory.user.created, async ({ user }) => {
  // Platform already created the user account
  // Platform already synced their profile data
  // Platform already assigned them to groups
  // Platform already logged the provisioning event

  console.log(`New user ${user.email} provisioned from ${user.directory.name}`)

  // You just handle your application-specific logic
  await assignDefaultDashboard(user)
  await sendWelcomeEmail(user)
})

The platform:

  1. Connects to customer's identity directory (Okta, Entra, Google Workspace, etc.)
  2. Syncs all existing users and groups
  3. Monitors for changes in real-time via webhooks
  4. Creates user accounts automatically when employees are added
  5. Updates profiles when employee information changes
  6. Deactivates accounts when employees leave the company
  7. Syncs group memberships for team-based permissions

Supported Directories

The platform works with 15+ enterprise directory providers automatically:

SCIM 2.0 Protocol:

  • Microsoft Entra ID (Azure AD)
  • Okta
  • OneLogin
  • JumpCloud
  • Ping Identity
  • Rippling
  • Generic SCIM 2.0

Native APIs:

  • Google Workspace
  • Microsoft 365
  • Workday
  • BambooHR
  • Namely
// No integration code needed
// Customer's IT admin connects their directory via admin portal
// Platform handles all sync logic automatically

Provisioning Events

The platform emits events for all directory changes:

User Created

on($.Directory.user.created, async ({ user, directory }) => {
  // New employee joined the company
  // Platform already created user account with:
  // - email, name, username
  // - organization membership
  // - initial group memberships
  // - profile attributes (department, title, etc.)

  console.log(`${user.email} provisioned from ${directory.name}`)

  // Handle your app-specific onboarding
  await createDefaultProject(user)
  await assignStarterResources(user)
  await sendOnboardingEmail(user)
})

User Updated

on($.Directory.user.updated, async ({ user, changes }) => {
  // Employee information changed in directory
  // Platform already updated user record

  console.log(`${user.email} updated:`, changes)
  // Example changes:
  // { department: { from: 'Sales', to: 'Engineering' } }
  // { title: { from: 'Associate', to: 'Senior' } }
  // { manager: { from: '[email protected]', to: '[email protected]' } }

  // React to meaningful changes
  if (changes.department) {
    await updateTeamAccess(user)
  }

  if (changes.title?.to?.includes('Senior')) {
    await grantAdvancedPermissions(user)
  }
})

User Deactivated

on($.Directory.user.deactivated, async ({ user }) => {
  // Employee left the company
  // Platform already:
  // - Marked account as inactive
  // - Revoked all sessions
  // - Removed from groups
  // - Logged deprovisioning event

  console.log(`${user.email} deactivated`)

  // Handle your app-specific offboarding
  await transferProjectOwnership(user)
  await archiveUserData(user)
  await notifyTeamMembers(user)
})

Group Membership

on($.Directory.group.userAdded, async ({ user, group }) => {
  // User was added to a group (team, department, etc.)
  // Platform already updated group membership

  console.log(`${user.email} added to ${group.name}`)

  // Map groups to permissions
  const roleMapping = {
    'Engineering': 'developer',
    'Product Managers': 'product-manager',
    'Administrators': 'admin'
  }

  const role = roleMapping[group.name]
  if (role) {
    await $.User.assign.Role({ userId: user.id, roleId: role })
  }
})

on($.Directory.group.userRemoved, async ({ user, group }) => {
  // User was removed from a group
  // Platform already updated membership

  console.log(`${user.email} removed from ${group.name}`)

  // Revoke associated permissions
  await updateAccessBasedOnGroupMembership(user)
})

User Data Model

Users provisioned from directories include rich profile data:

{
  id: 'user_01ABC...',
  email: '[email protected]',
  firstName: 'Jane',
  lastName: 'Doe',
  username: 'jane.doe',
  state: 'active', // or 'inactive'

  // Organization context
  organizationId: 'org_01XYZ...',
  organization: {
    id: 'org_01XYZ...',
    name: 'Acme Corporation'
  },

  // Directory context
  directoryId: 'directory_01DEF...',
  idpId: 'okta-user-id-12345',

  // Profile attributes from directory
  profile: {
    department: 'Engineering',
    title: 'Senior Software Engineer',
    employeeId: 'EMP-12345',
    manager: '[email protected]',
    location: 'San Francisco',
    phoneNumber: '+1 (555) 123-4567',
    startDate: '2024-01-15',
    costCenter: 'ENG-001'
  },

  // Group memberships
  groups: [
    { id: 'group_01...',name: 'Engineering' },
    { id: 'group_02...', name: 'Backend Team' },
    { id: 'group_03...', name: 'San Francisco Office' }
  ],

  // Timestamps
  createdAt: '2025-01-20T10:00:00.000Z',
  updatedAt: '2025-01-20T10:00:00.000Z',
  lastLoginAt: '2025-01-20T14:30:00.000Z'
}

Group Synchronization

Groups from directories map to teams/departments in your app:

// Platform automatically syncs groups as well as users
on($.Directory.group.created, async ({ group }) => {
  // New team/department created in directory
  // Platform already created group record

  console.log(`New group: ${group.name}`)

  // Create corresponding team in your app
  await $.Team.create({
    name: group.name,
    organizationId: group.organizationId,
    directoryGroupId: group.id,
    type: inferGroupType(group.name) // 'department', 'team', 'location'
  })
})

// Access control based on groups
on($.API.request, async ({ user, resource }) => {
  const userGroups = user.groups.map(g => g.name)

  // Check if user's groups have access
  if (resource.requiresGroup && !userGroups.includes(resource.requiresGroup)) {
    throw new Error(`Access denied - must be in ${resource.requiresGroup}`)
  }
})

Role-Based Access Control

Automatically assign roles based on directory groups:

// Define group-to-role mappings
const groupRoles = {
  'Engineering': ['developer', 'api-access'],
  'Engineering - Senior': ['developer', 'api-access', 'code-review', 'deploy'],
  'Product Management': ['product-manager', 'analytics'],
  'Sales': ['sales', 'crm-access'],
  'Support': ['support', 'ticket-access'],
  'Administrators': ['admin']
}

// Apply automatically when user joins group
on($.Directory.group.userAdded, async ({ user, group }) => {
  const roles = groupRoles[group.name] || []

  for (const roleId of roles) {
    await $.User.assign.Role({ userId: user.id, roleId })
  }

  console.log(`Assigned ${roles.length} roles to ${user.email} based on ${group.name}`)
})

// Remove when user leaves group
on($.Directory.group.userRemoved, async ({ user, group }) => {
  const roles = groupRoles[group.name] || []

  for (const roleId of roles) {
    await $.User.unassign.Role({ userId: user.id, roleId })
  }
})

Attribute Mapping

Map directory-specific attributes to your data model:

on($.Directory.user.synced, async ({ user, rawAttributes }) => {
  // Platform provides standardized fields (email, name, etc.)
  // Plus raw attributes from the directory provider

  const mappedData = {
    // Standard fields (already mapped by platform)
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,

    // Map provider-specific attributes
    department: rawAttributes.department ||
                rawAttributes['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department'],

    jobTitle: rawAttributes.title || rawAttributes.jobTitle,

    employeeId: rawAttributes.employeeNumber || rawAttributes.employeeId,

    managerId: await findUserByEmail(rawAttributes.manager?.value || rawAttributes.manager),

    office: rawAttributes.location ||
            rawAttributes.physicalDeliveryOfficeName ||
            rawAttributes.officeLocation,

    // Okta-specific custom attributes
    division: rawAttributes['urn:okta:custom:user:1.0:division'],

    // Azure AD-specific attributes
    costCenter: rawAttributes['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter'],

    // Google Workspace-specific
    orgUnit: rawAttributes.orgUnitPath
  }

  // Update user with mapped data
  await $.User.update(user.id, mappedData)
})

Deprovisioning

Handle employee offboarding automatically:

on($.Directory.user.deleted, async ({ user }) => {
  // Employee no longer in directory
  // Platform already:
  // - Marked account inactive
  // - Revoked all sessions
  // - Removed group memberships
  // - Logged deprovisioning

  console.log(`Deprovisioning ${user.email}`)

  // Transfer ownership of resources
  const projects = await $.Project.list({ ownerId: user.id })
  for (const project of projects) {
    const newOwner = await findProjectSuccessor(project, user)
    await $.Project.update(project.id, { ownerId: newOwner.id })
  }

  // Archive user data
  await $.Data.archive({
    userId: user.id,
    reason: 'user_deprovisioned',
    retentionPeriod: '90d'
  })

  // Notify relevant teams
  await $.Notification.send({
    to: user.manager,
    type: 'employee_departed',
    data: { user, projects }
  })
})

Initial Directory Sync

When a directory is first connected:

on($.Directory.activated, async ({ directory }) => {
  // Platform automatically:
  // 1. Fetches all users from directory (paginated)
  // 2. Creates user accounts for each
  // 3. Fetches all groups
  // 4. Assigns users to groups
  // 5. Sets directory as active

  console.log(`Directory ${directory.name} activated for ${directory.organization.name}`)

  // You can react to initial sync completion
  const userCount = await $.User.count({ directoryId: directory.id })
  const groupCount = await $.Group.count({ directoryId: directory.id })

  console.log(`Synced ${userCount} users and ${groupCount} groups`)

  // Send notification to organization admin
  await $.Email.send({
    to: directory.organization.adminEmail,
    template: 'directory-sync-complete',
    data: { directory, userCount, groupCount }
  })
})

Real-Time Synchronization

Changes sync in real-time via webhooks:

// Employee added to directory → User created in < 5 seconds
// Employee updated → Profile updated immediately
// Employee removed → Account deactivated immediately
// Group membership changed → Permissions updated immediately

// No polling, no delays, no manual sync buttons

on($.Directory.syncCompleted, async ({ directory, stats }) => {
  // Platform synced changes
  console.log(`Sync completed for ${directory.name}`, {
    usersCreated: stats.usersCreated,
    usersUpdated: stats.usersUpdated,
    usersDeleted: stats.usersDeleted,
    groupsCreated: stats.groupsCreated,
    membershipChanges: stats.membershipChanges,
    duration: stats.duration
  })
})

Conflict Resolution

The platform handles sync conflicts automatically:

// What happens when:
// - User exists in both directory and your app?
//   → Platform matches by email and updates existing user
//
// - User deleted from directory but owns resources?
//   → Platform marks inactive but preserves data
//
// - User in multiple directories?
//   → Platform uses primary directory configured by org
//
// - Directory temporarily unavailable?
//   → Platform retries with exponential backoff
//   → Existing users continue working
//   → New employees added when directory returns

// All handled automatically - no code needed

Provisioning Filters

Optionally filter which users/groups to sync:

// Configure filters (typically done via admin portal)
await $.Organization.update(orgId, {
  directoryConfig: {
    syncFilters: {
      // Only sync users in specific groups
      includeGroups: ['Engineering', 'Product', 'Design'],

      // Exclude certain groups
      excludeGroups: ['Contractors', 'Terminated'],

      // Only sync active users
      includeActive: true,

      // Custom attribute filters
      customFilters: {
        employeeType: ['Full-Time', 'Part-Time']
      }
    }
  }
})

// Platform applies filters automatically
// Only matching users are provisioned

Custom Provisioning Logic

Extend default provisioning behavior:

on($.Directory.user.beforeCreate, async ({ user, directory }) => {
  // Intercept before user creation
  // Add custom logic

  // Example: Set default preferences based on department
  const preferences = {
    'Engineering': { theme: 'dark', notifications: 'minimal' },
    'Sales': { theme: 'light', notifications: 'all' },
    'Support': { theme: 'light', notifications: 'important' }
  }

  user.preferences = preferences[user.profile.department] || {}

  // Example: Assign to projects based on location
  if (user.profile.location === 'San Francisco') {
    user.defaultProjects = ['project_sf_01', 'project_sf_02']
  }

  return user
})

on($.Directory.user.afterCreate, async ({ user }) => {
  // After user creation
  // Trigger onboarding workflows

  await $.Workflow.start('user-onboarding', { userId: user.id })

  // Create starter resources
  await $.Dashboard.create({
    name: `${user.firstName}'s Dashboard`,
    ownerId: user.id,
    type: 'personal'
  })
})

Monitoring

Monitor provisioning health:

// Get provisioning metrics
const metrics = await $.Analytics.get({
  metric: 'directory.sync',
  organizationId: orgId,
  period: 'last_30_days'
})

console.log({
  totalSyncs: metrics.syncCount,
  usersProvisioned: metrics.usersCreated,
  usersDeprovisioned: metrics.usersDeleted,
  avgSyncTime: metrics.avgDuration,
  errorRate: metrics.errorRate,
  lastSyncAt: metrics.lastSync
})

// Alert on sync failures
on($.Directory.syncFailed, async ({ directory, error }) => {
  await $.Alert.send({
    type: 'integration',
    severity: 'high',
    message: `Directory sync failed for ${directory.organization.name}`,
    details: { directory, error }
  })
})

Security

Provisioning is secure by default:

  • Encrypted Connection: All directory communications use TLS
  • Credential Storage: Directory credentials encrypted at rest
  • Audit Trail: All provisioning events logged
  • Minimal Permissions: Only read access to directory data
  • Tenant Isolation: Each organization's directory is isolated
  • Webhook Validation: All incoming webhooks cryptographically verified
// All security handled by platform
// No credentials stored in your application code
// No direct directory access

Cost Model

Directory sync is included in platform pricing:

  • No per-user fees - Sync unlimited users
  • No sync fees - Real-time updates included
  • No API call fees - Unlimited directory API calls
  • No storage fees - User data storage included

Development & Testing

Provisioning works in development too:

// Platform provides test directories for development
if (process.env.NODE_ENV === 'development') {
  // Use test directory that simulates provisioning
  const testDirectory = await $.Directory.createTest({
    users: 10,
    groups: 3
  })

  // Simulate directory events
  await $.Directory.simulate('user.created', {
    email: '[email protected]',
    firstName: 'Test',
    lastName: 'User'
  })
}

Migration

Moving from manual user management to automatic provisioning:

Before (Manual)

// Manual user creation
app.post('/users', requireAdmin, async (req, res) => {
  const user = await db.users.create(req.body)
  await sendInviteEmail(user)
  res.json({ user })
})

// Manual deactivation
app.post('/users/:id/deactivate', requireAdmin, async (req, res) => {
  await db.users.update(req.params.id, { active: false })
  await revokeAccess(req.params.id)
  res.json({ success: true })
})

// Manual sync script run weekly
cron.schedule('0 0 * * 0', async () => {
  const directoryUsers = await fetchFromDirectory()
  await reconcileUsers(directoryUsers)
})

After (Automatic)

// Users are created automatically
on($.Directory.user.created, async ({ user }) => {
  // User already created by platform
  // Just handle app-specific logic
  await createDefaultResources(user)
})

// Users are deactivated automatically
on($.Directory.user.deleted, async ({ user }) => {
  // User already deactivated by platform
  // Just handle app-specific cleanup
  await archiveUserData(user)
})

// Sync happens in real-time automatically
// No cron jobs needed

Summary

User provisioning on the .do platform is:

  • Automatic - No manual user creation or deletion
  • Real-Time - Changes sync within seconds
  • Comprehensive - Users, groups, and attributes
  • Secure - Encrypted, audited, isolated
  • Reliable - Automatic retries, conflict resolution
  • Simple - No provisioning code to write

Focus on your application features. Let the platform handle user lifecycle management.