Skip to content
Tolinku
Tolinku
Sign In Start Free
App Growth · · 6 min read

A/B Testing Onboarding Flows with Deep Links

By Tolinku Staff
|
Tolinku referral programs dashboard screenshot for growth blog posts

Users who arrive via deep links have context: they clicked a specific link, from a specific source, expecting specific content. Their onboarding should reflect that context. A/B testing onboarding flows for deep-linked users is high-leverage because these users have demonstrated intent, and the gap between "interested" and "activated" is where most of them are lost.

For onboarding fundamentals, see User Onboarding with Deep Links: Complete Guide. For onboarding A/B testing patterns, see A/B Testing Onboarding Flows.

Tolinku A/B test creation form with type, goal, and variant configuration The A/B test creation form with test type, goal metric, route picker, and variant builder.

What to Test

High-Impact Onboarding Variables

Variable Expected Impact Test Difficulty
Number of steps (2 vs. 3 vs. 4) 10-20% completion lift Easy
Signup method order (social vs. email) 5-15% signup lift Easy
Personalized vs. generic welcome 10-25% completion lift Medium
Content-first vs. auth-first 15-35% activation lift Medium
Permission request timing 15-30% grant rate lift Easy
Skip option availability 5-15% activation lift Easy
First action type 10-20% D7 retention lift Medium

These tests only apply to users arriving via deep links:

Variable What You're Testing
Show deferred content immediately vs. after signup Does seeing the content increase signup rate?
Personalized welcome vs. standard Does referrer context improve completion?
Skip onboarding entirely vs. minimal flow For returning users with the app reinstalled
Pre-filled data vs. empty forms Does pre-filling from deep link params reduce friction?

Implementation

Experiment Configuration

const onboardingExperiments = {
  step_count_v3: {
    id: 'step_count_v3',
    variants: [
      {
        id: 'two_steps',
        weight: 50,
        config: {
          steps: ['signup', 'first_action'],
          skipProfileSetup: true,
          skipInterests: true,
        },
      },
      {
        id: 'three_steps',
        weight: 50,
        config: {
          steps: ['signup', 'profile', 'first_action'],
          skipProfileSetup: false,
          skipInterests: true,
        },
      },
    ],
    primaryMetric: 'activation', // Core action completed
    secondaryMetrics: ['d1_retention', 'd7_retention', 'onboarding_completion'],
    segmentBy: ['source', 'has_deferred_link'],
  },

  auth_method_v2: {
    id: 'auth_method_v2',
    variants: [
      {
        id: 'social_first',
        weight: 50,
        config: {
          primaryAuth: ['apple', 'google'],
          secondaryAuth: ['email'],
          showPasskey: true,
        },
      },
      {
        id: 'email_first',
        weight: 50,
        config: {
          primaryAuth: ['email'],
          secondaryAuth: ['apple', 'google'],
          showPasskey: true,
        },
      },
    ],
    primaryMetric: 'signup_completed',
    secondaryMetrics: ['time_to_signup', 'drop_off_step'],
  },
};
async function routeOnboarding(userId) {
  const deferredLink = await checkDeferredDeepLink();
  const experiments = getActiveOnboardingExperiments();

  // Assign variants for all active experiments
  const assignments = {};
  for (const exp of experiments) {
    assignments[exp.id] = assignVariant(userId, exp.id);
  }

  // Build onboarding config based on all assignments
  const config = mergeExperimentConfigs(assignments);

  // Add deep link context
  if (deferredLink) {
    config.deferredContent = deferredLink.params;
    config.source = deferredLink.params.utm_source || 'deep_link';
    config.welcomeMessage = getContextualWelcome(deferredLink.params);
  }

  return config;
}

function getContextualWelcome(params) {
  if (params.referrer_name) {
    return `${params.referrer_name} invited you to join.`;
  }
  if (params.product_id) {
    return 'Sign up to see what you were looking at.';
  }
  if (params.promo_code) {
    return 'Create an account to claim your offer.';
  }
  return 'Welcome! Let\'s get you started.';
}

Content-First vs. Auth-First

const contentFirstExperiment = {
  id: 'content_first_v2',
  // Only applies to users with deferred deep link content
  eligibility: (user) => user.hasDeferredContent,
  variants: [
    {
      id: 'content_first',
      weight: 50,
      config: {
        flow: [
          'show_deferred_content', // Show the product/article/content they clicked on
          'signup_prompt',          // "Sign up to save this / continue"
          'first_action',           // Core action
        ],
      },
    },
    {
      id: 'auth_first',
      weight: 50,
      config: {
        flow: [
          'signup',                 // Sign up first
          'show_deferred_content',  // Then show the content
          'first_action',
        ],
      },
    },
  ],
  primaryMetric: 'signup_completed',
  secondaryMetrics: ['d7_retention', 'content_engagement'],
};

Typical result: Content-first wins by 20-35% for signup completion. Users who see value before signing up are more willing to create an account. Auth-first wins only when the content is time-sensitive (limited offer, expiring invite).

Permission Request Timing

const permissionTimingExperiment = {
  id: 'permission_timing_v1',
  variants: [
    {
      id: 'during_onboarding',
      weight: 50,
      config: {
        requestPushPermission: 'onboarding_step_3',
        requestLocationPermission: 'onboarding_step_4',
      },
    },
    {
      id: 'contextual',
      weight: 50,
      config: {
        requestPushPermission: 'after_first_save', // "Want to know when this is back in stock?"
        requestLocationPermission: 'on_store_locator', // "Allow location to find stores near you"
      },
    },
  ],
  primaryMetric: 'push_permission_granted',
  secondaryMetrics: ['location_permission_granted', 'onboarding_completion'],
};

Contextual permission requests grant rates are typically 15-30% higher than during-onboarding requests.

Tracking Onboarding Experiments

Event Schema

// Step started
analytics.track('onboarding_step_started', {
  experimentId,
  variantId,
  step: 'signup',
  stepIndex: 0,
  totalSteps: 3,
  source: deferredLink ? 'deep_link' : 'organic',
  hasDeferredContent: !!deferredLink,
});

// Step completed
analytics.track('onboarding_step_completed', {
  experimentId,
  variantId,
  step: 'signup',
  stepIndex: 0,
  duration: stepDurationMs,
  method: 'google_sign_in', // or 'email', 'apple', 'passkey'
});

// Onboarding completed
analytics.track('onboarding_completed', {
  experimentId,
  variantId,
  totalDuration: totalDurationMs,
  stepsCompleted: 3,
  stepsSkipped: 0,
  source: deferredLink ? 'deep_link' : 'organic',
});

// Activation (the metric that actually matters)
analytics.track('user_activated', {
  experimentId,
  variantId,
  activationAction: 'created_first_item',
  timeFromSignup: activationDelayMs,
  source: deferredLink ? 'deep_link' : 'organic',
});

Drop-Off Analysis

async function analyzeDropOff(experimentId) {
  const experiment = getExperiment(experimentId);

  for (const variant of experiment.variants) {
    const steps = variant.config.steps || variant.config.flow;
    const funnel = [];

    for (let i = 0; i < steps.length; i++) {
      const started = await countEvents('onboarding_step_started', {
        experimentId,
        variantId: variant.id,
        stepIndex: i,
      });
      const completed = await countEvents('onboarding_step_completed', {
        experimentId,
        variantId: variant.id,
        stepIndex: i,
      });

      funnel.push({
        step: steps[i],
        started,
        completed,
        completionRate: (completed / started * 100).toFixed(1) + '%',
        dropOff: started - completed,
      });
    }

    console.log(`\n${variant.id}:`);
    funnel.forEach(f => {
      console.log(`  ${f.step}: ${f.completionRate} (${f.dropOff} dropped)`);
    });
  }
}

Common Test Patterns

Pattern 1: Referral Onboarding

Users arriving via referral links need a different flow:

const referralOnboardingTest = {
  id: 'referral_onboarding_v1',
  eligibility: (user) => user.deferredParams?.ref,
  variants: [
    {
      id: 'referral_context',
      weight: 50,
      config: {
        showReferrerInfo: true,     // "Sarah invited you"
        showReferralReward: true,   // "You both get $10"
        prefillReferralCode: true,
        steps: ['signup', 'claim_reward'],
      },
    },
    {
      id: 'standard',
      weight: 50,
      config: {
        showReferrerInfo: false,
        showReferralReward: false,
        prefillReferralCode: true,  // Still apply the code silently
        steps: ['signup', 'first_action'],
      },
    },
  ],
  primaryMetric: 'signup_completed',
  secondaryMetrics: ['referral_reward_claimed', 'd7_retention'],
};

Pattern 2: Campaign-Specific Onboarding

Users from ad campaigns see campaign-aligned onboarding:

const campaignOnboardingTest = {
  id: 'campaign_onboarding_v1',
  eligibility: (user) => user.deferredParams?.utm_campaign,
  variants: [
    {
      id: 'campaign_aligned',
      weight: 50,
      config: {
        welcomeHeadline: 'dynamicFromCampaign', // Matches the ad they clicked
        firstScreen: 'campaign_content',         // Show what the ad promised
        skipGenericOnboarding: true,
      },
    },
    {
      id: 'standard',
      weight: 50,
      config: {
        welcomeHeadline: 'Welcome to [App]',
        firstScreen: 'standard_onboarding',
        skipGenericOnboarding: false,
      },
    },
  ],
  primaryMetric: 'activation',
};

Pattern 3: Progressive Profiling

Collect user data over multiple sessions instead of all at once:

const profilingTest = {
  id: 'progressive_profiling_v1',
  variants: [
    {
      id: 'upfront',
      weight: 50,
      config: {
        collectDuringOnboarding: ['name', 'interests', 'notifications'],
      },
    },
    {
      id: 'progressive',
      weight: 50,
      config: {
        collectDuringOnboarding: ['name'],
        collectOnSession2: ['interests'],
        collectOnSession3: ['notifications'],
      },
    },
  ],
  primaryMetric: 'd7_retention',
  secondaryMetrics: ['profile_completion_d30', 'notification_opt_in_d7'],
};

Progressive profiling typically produces 10-15% higher D7 retention because users don't feel overwhelmed, but 20-30% lower notification opt-in rates at day 1 (which catches up by day 7-14 in most cases).

Metrics That Matter

Primary: Activation Rate

Don't optimize for onboarding completion. Optimize for activation (the action most correlated with retention):

App Type Activation Action Typical Timeline
E-commerce First purchase Within 7 days
Social First post or connection Within 3 days
Productivity First project created Within 1 day
Content 3+ articles read Within 7 days
Gaming Level 5 reached Within 3 days

Secondary: Retention Cohorts

Track D1, D7, and D30 retention for each variant. An onboarding variant that has 5% higher completion but 10% lower D7 retention is a worse outcome.

async function retentionByVariant(experimentId, day) {
  for (const variant of getVariants(experimentId)) {
    const signedUp = await getUsersInVariant(experimentId, variant.id);
    const retained = await getUsersRetainedOnDay(signedUp, day);

    console.log(variant.id, {
      users: signedUp.length,
      retained: retained.length,
      rate: (retained.length / signedUp.length * 100).toFixed(1) + '%',
    });
  }
}

Best Practices

  1. Measure activation, not completion: Onboarding completion is a vanity metric. Activation and retention are what matter.
  2. Segment deep link users from organic: Deep-linked users have context and intent. Their optimal onboarding is different.
  3. Test content-first for deep link users: If a user clicked to see specific content, show it before asking them to sign up.
  4. Run for at least 30 days: Onboarding experiments need retention data, which takes weeks to materialize.
  5. Don't test too many things at once: Change one major flow element per test. Save multivariate testing for when you have high enough traffic.
  6. Pre-fill everything you can: Deep link parameters often contain data (referral codes, product IDs, promo codes) that can pre-fill forms and reduce friction.

For A/B testing features, see Tolinku A/B testing. For onboarding use cases, see the onboarding docs.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.