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:
- Connects to customer's identity directory (Okta, Entra, Google Workspace, etc.)
- Syncs all existing users and groups
- Monitors for changes in real-time via webhooks
- Creates user accounts automatically when employees are added
- Updates profiles when employee information changes
- Deactivates accounts when employees leave the company
- 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 automaticallyProvisioning 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 neededProvisioning 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 provisionedCustom 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 accessCost 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 neededSummary
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.
Related Documentation
- Authentication - SSO and identity integration
- Multi-Tenancy - Organization isolation
- Admin Portals - Self-service directory configuration
- Compliance - Provisioning audit trails
- API Reference - User primitive API
Vault & Secrets Management
Built-in secrets management - store API keys, credentials, and sensitive configuration securely with automatic encryption and access control
Multi-Tenancy
Built-in organization isolation, per-tenant data security, and resource management - multi-tenancy is automatic on the .do platform