Enterprise mobile app onboarding is fundamentally different from consumer app onboarding. The user didn't choose to install the app; their IT department deployed it. They don't need to be "sold" on the product; they need to understand how to use it for their job. And the onboarding must satisfy IT administrators, security policies, and compliance requirements.
For improving completion rates, see Improving Onboarding Completion Rates. For freemium approaches, see Onboarding for Freemium Apps: Free to Paid Journey. For general onboarding principles, see Onboarding Best Practices for Mobile Apps in 2026.
Enterprise Onboarding Differences
| Aspect | Consumer App | Enterprise App |
|---|---|---|
| Install motivation | User chose the app | IT deployed the app |
| Account creation | Email + password signup | SSO, SAML, SCIM provisioning |
| Configuration | User preferences | IT admin policies |
| Feature access | Same for everyone | Role-based |
| Compliance | Terms of service | Data handling policy, NDAs |
| Training | Self-service | May require guided training |
| Support | Help center | IT helpdesk + vendor support |
SSO and Provisioned Onboarding
SSO Login Flow
Enterprise users authenticate via their company's identity provider:
async function handleEnterpriseLogin() {
// Check if the device is managed (MDM)
const mdmConfig = await getMDMConfiguration();
if (mdmConfig && mdmConfig.ssoProvider) {
// Auto-redirect to company SSO
return startSSOFlow(mdmConfig.ssoProvider, mdmConfig.tenantId);
}
// Manual SSO discovery via email domain
return (
<Screen>
<Heading>Sign in with your work email</Heading>
<Input
label="Work Email"
type="email"
onSubmit={async (email) => {
const domain = email.split('@')[1];
const ssoConfig = await discoverSSO(domain);
if (ssoConfig) {
startSSOFlow(ssoConfig.provider, ssoConfig.tenantId);
} else {
showError('Your organization has not been configured. Contact your IT admin.');
}
}}
/>
</Screen>
);
}
async function startSSOFlow(provider, tenantId) {
// Redirect to identity provider
const authUrl = buildSSOUrl(provider, tenantId, {
redirectUri: 'yourapp://auth/callback',
scope: 'openid profile email',
});
await openAuthSession(authUrl);
}
SCIM-Provisioned Users
Users provisioned via SCIM already have accounts:
async function handleProvisionedUser(ssoToken) {
const userInfo = await validateSSOToken(ssoToken);
const provisionedUser = await findProvisionedUser(userInfo.email);
if (provisionedUser) {
// Account exists, log in and apply role-based config
await loginUser(provisionedUser);
const roleConfig = await getRoleConfig(provisionedUser.role);
return startRoleBasedOnboarding(provisionedUser, roleConfig);
}
// Not provisioned yet, but SSO is valid
return showPendingProvisioningScreen(userInfo);
}
MDM-Based Onboarding
Reading MDM Configuration
When the app is deployed via Mobile Device Management:
async function readMDMConfig() {
// iOS: App Config from MDM profile
// Android: Android Enterprise managed configurations
const config = await getAppManagedConfig();
return {
tenantId: config.tenant_id,
ssoProvider: config.sso_provider,
apiEndpoint: config.api_endpoint,
features: config.enabled_features ? config.enabled_features.split(',') : [],
complianceRequired: config.require_compliance === 'true',
dataPolicy: config.data_handling_policy_url,
};
}
Zero-Touch Setup
For MDM-deployed apps, the goal is zero-touch onboarding:
async function zeroTouchOnboarding() {
const mdmConfig = await readMDMConfig();
// 1. Configure the app
await configureAppEndpoint(mdmConfig.apiEndpoint);
// 2. Auto-authenticate via MDM certificate or SSO
const user = await authenticateViaMDM(mdmConfig);
// 3. Apply feature restrictions
await applyFeaturePolicy(mdmConfig.features);
// 4. Show compliance acknowledgment if required
if (mdmConfig.complianceRequired) {
await showComplianceAcknowledgment(mdmConfig.dataPolicy);
}
// 5. Navigate to the role-appropriate home screen
navigation.navigate(getRoleHomeScreen(user.role));
}
Role-Based Onboarding
Defining Role Flows
Different roles need different onboarding experiences:
const roleOnboarding = {
admin: {
steps: ['dashboard_overview', 'team_management', 'settings', 'integrations'],
firstScreen: 'AdminDashboard',
features: ['all'],
},
manager: {
steps: ['team_view', 'reports', 'approvals'],
firstScreen: 'TeamOverview',
features: ['reports', 'approvals', 'team_view', 'projects'],
},
member: {
steps: ['task_overview', 'communication', 'time_tracking'],
firstScreen: 'MyTasks',
features: ['tasks', 'communication', 'time_tracking'],
},
viewer: {
steps: ['dashboard_view', 'reports'],
firstScreen: 'ReadOnlyDashboard',
features: ['view_only'],
},
};
function getRoleOnboarding(role) {
return roleOnboarding[role] || roleOnboarding.member;
}
Role-Based Feature Tour
function RoleFeatureTour({ role }) {
const config = getRoleOnboarding(role);
const tourSteps = config.steps.map(step => ({
...featureTourContent[step],
visible: config.features.includes('all') || config.features.includes(step),
})).filter(s => s.visible);
return (
<FeatureTour
steps={tourSteps}
onComplete={() => navigation.navigate(config.firstScreen)}
/>
);
}
Deep Links for Enterprise Onboarding
Invite Links for Teams
Enterprise deep links typically involve team invitations. For a broader look at deep linking in B2B contexts, see Deep Linking for B2B Apps.
async function createTeamInviteLink(admin, inviteeEmail, role) {
const inviteToken = await createInviteToken({
tenantId: admin.tenantId,
inviterId: admin.id,
inviteeEmail: inviteeEmail,
role: role,
expiresIn: '7d',
});
const link = await Tolinku.createLink({
path: `/join/${admin.tenantId}`,
params: {
invite: inviteToken,
role: role,
org: admin.organizationName,
},
ogTitle: `Join ${admin.organizationName} on [App]`,
ogDescription: `${admin.name} invited you to join as ${role}.`,
});
return link.url;
}
Handling Enterprise Deep Links
async function handleEnterpriseDeepLink(deferred) {
if (deferred === null) return startStandardLogin();
const params = deferred.params;
if (params.invite) {
// Validate invite token
const invite = await validateInviteToken(params.invite);
if (invite === null) {
return showError('This invite link has expired. Ask your admin for a new one.');
}
if (invite.expired) {
return showError('This invite link has expired. Ask your admin for a new one.');
}
// Pre-configure based on invite
return {
ssoProvider: invite.ssoProvider,
tenantId: invite.tenantId,
role: invite.role,
organizationName: invite.organizationName,
};
}
}
Deep Links to Specific Resources
Enterprise users often receive deep links to specific tasks, documents, or projects:
function handleResourceDeepLink(url) {
const path = new URL(url).pathname;
// Authenticate first
if (user.isAuthenticated === false) {
pendingDeepLink.save(url);
navigation.navigate('SSOLogin');
return;
}
// Check access
const resource = parseResourcePath(path);
const hasAccess = checkResourceAccess(user, resource);
if (hasAccess === false) {
showAccessDenied(resource);
return;
}
navigation.navigate(resource.screen, resource.params);
}
Compliance and Policy Acknowledgment
Required Acknowledgments
Enterprise apps often require users to acknowledge policies before using the app. For compliance-specific guidance, see Onboarding and Compliance: Handling Regulations.
async function checkComplianceOnboarding(user) {
const requiredPolicies = await getRequiredPolicies(user.tenantId);
const acknowledged = await getAcknowledgedPolicies(user.id);
const pending = requiredPolicies.filter(
policy => acknowledged.includes(policy.id) === false
);
if (pending.length > 0) {
return pending;
}
return null;
}
function PolicyAcknowledgmentScreen({ policies, onComplete }) {
const [currentPolicy, setCurrentPolicy] = useState(0);
const policy = policies[currentPolicy];
return (
<Screen>
<Heading>{policy.title}</Heading>
<ScrollView>
<PolicyContent content={policy.content} />
</ScrollView>
<Checkbox
label={`I have read and acknowledge the ${policy.title}`}
onChange={(checked) => {
if (checked) {
acknowledgePolicy(policy.id);
if (currentPolicy < policies.length - 1) {
setCurrentPolicy(currentPolicy + 1);
} else {
onComplete();
}
}
}}
/>
</Screen>
);
}
Data Classification
Enterprise apps may need to inform users about data handling:
function DataClassificationNotice({ tenantConfig }) {
return (
<Notice>
<Heading>Data Handling Notice</Heading>
<Text>
Data entered in this app is classified as {tenantConfig.dataClassification}
and is stored in {tenantConfig.dataRegion}.
</Text>
<Text>
Do not enter {tenantConfig.restrictedDataTypes.join(', ')} in this app.
</Text>
<Button onPress={acknowledge}>I Understand</Button>
</Notice>
);
}
Measuring Enterprise Onboarding
Admin-Facing Metrics
| Metric | Definition | Target |
|---|---|---|
| Deployment completion | Users who completed setup / Users provisioned | > 90% |
| Time to first use | Provisioned to first action | < 24 hours |
| SSO success rate | Successful SSO logins / Attempts | > 95% |
| Policy acknowledgment rate | Policies acknowledged / Required | 100% |
| Support ticket rate | Tickets during onboarding / Total users | < 5% |
Deployment Tracking
async function deploymentReport(tenantId) {
const provisioned = await countProvisionedUsers(tenantId);
const completed = await countCompletedOnboarding(tenantId);
const active = await countActiveUsers(tenantId, '7d');
const tickets = await countSupportTickets(tenantId, 'onboarding');
return {
provisioned,
onboardingCompletion: (completed / provisioned * 100).toFixed(1) + '%',
activeRate: (active / provisioned * 100).toFixed(1) + '%',
supportTicketRate: (tickets / provisioned * 100).toFixed(1) + '%',
};
}
For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.
Get deep linking tips in your inbox
One email per week. No spam.