Users leave and come back. Some return after a week, some after months. When they return, they face a problem: the app may have changed, their context is gone, and they've forgotten how things work. Re-onboarding is the process of getting returning users back up to speed without treating them like new users. Deep links are the mechanism that brings them back, and the context in those links determines what they see.
For building growth loops, see App Growth Loops: Building Self-Sustaining Growth. For budget-friendly strategies, see App Growth for Startups: Budget-Friendly Strategies. For win-back campaign design, see Re-Engagement Campaigns: Winning Back Lapsed Users.
Identifying Returning Users
User Segments
| Segment | Absence Duration | Likely State | Re-Onboarding Need |
|---|---|---|---|
| Brief lapse | 7-14 days | Remember the app | Minimal (what's new) |
| Moderate lapse | 15-30 days | Partially remember | Light re-onboarding |
| Long lapse | 31-90 days | Forgot most features | Moderate re-onboarding |
| Churned | 90+ days | Don't remember | Near-full re-onboarding |
Detection
async function detectReturningUser(userId) {
const user = await getUser(userId);
const daysSinceLastActive = daysBetween(user.lastActiveAt, new Date());
if (daysSinceLastActive < 7) {
return { type: 'active', needsReOnboarding: false };
}
if (daysSinceLastActive < 15) {
return { type: 'brief_lapse', needsReOnboarding: false, showWhatsNew: true };
}
if (daysSinceLastActive < 31) {
return { type: 'moderate_lapse', needsReOnboarding: true, level: 'light' };
}
if (daysSinceLastActive < 91) {
return { type: 'long_lapse', needsReOnboarding: true, level: 'moderate' };
}
return { type: 'churned', needsReOnboarding: true, level: 'full' };
}
Re-Onboarding Flows
Brief Lapse (7-14 Days): What's New
For users who were recently active, just highlight changes:
function WhatsNewScreen({ changes }) {
return (
<BottomSheet>
<Heading>What's new since you were last here</Heading>
{changes.map(change => (
<ChangeItem key={change.id}>
<Icon name={change.icon} />
<View>
<Text style={styles.changeTitle}>{change.title}</Text>
<Text style={styles.changeDescription}>{change.description}</Text>
</View>
</ChangeItem>
))}
<Button onPress={dismiss}>Got It</Button>
</BottomSheet>
);
}
async function getChanges(lastActiveDate) {
const allChanges = await getAppChangelog();
return allChanges.filter(c => new Date(c.date) > lastActiveDate);
}
Moderate Lapse (15-30 Days): Guided Return
Show the user their existing data and remind them of key features:
function ModerateReOnboarding({ user }) {
return (
<View>
<WelcomeBackScreen
userName={user.firstName}
lastSeen={formatRelative(user.lastActiveAt)}
/>
<DataSummary
items={user.itemCount}
projects={user.projectCount}
lastActivity={user.lastActivity}
/>
<QuickActions
actions={[
{ label: 'Continue where you left off', deepLink: user.lastScreen },
{ label: 'See what is new', deepLink: '/changelog' },
{ label: 'Start fresh', deepLink: '/home' },
]}
/>
</View>
);
}
Long Lapse (31-90 Days): Feature Reintroduction
The user has likely forgotten features. Show a condensed version of the onboarding:
function LongLapseReOnboarding({ user }) {
const newFeatures = getFeaturesSince(user.lastActiveAt);
const coreFeatures = getCoreFeatures();
return (
<View>
<WelcomeBackScreen
userName={user.firstName}
message="A lot has happened since you were last here."
/>
{newFeatures.length > 0 && (
<NewFeaturesCarousel features={newFeatures} />
)}
<FeatureRefresher
features={coreFeatures}
format="quick_tips" // Short tooltips, not full tour
/>
<DataPreservation
message="All your data is still here."
stats={{
projects: user.projectCount,
items: user.itemCount,
}}
/>
<Button onPress={() => navigation.navigate('Home')}>
Let's Go
</Button>
</View>
);
}
Churned (90+ Days): Near-Full Re-Onboarding
Users absent for 3+ months need substantial re-onboarding, but not from scratch:
function ChurnedReOnboarding({ user }) {
return (
<View>
<WelcomeBackScreen
userName={user.firstName}
message="Welcome back! Here is a quick refresher."
/>
{/* Condensed feature tour (3 screens, not 7) */}
<CondensedTour
features={getTopFeatures(3)}
format="highlight"
/>
{/* Check if their data is still relevant */}
<DataCheck
onKeepData={() => navigation.navigate('Home')}
onStartFresh={() => resetUserData(user.id)}
/>
{/* Offer to update preferences */}
<PreferenceRefresh user={user} />
</View>
);
}
Deep Links for Re-Engagement
The channels you use to bring users back (push, email, in-app) each require different strategies. For push-specific guidance, see Push Notification Strategy for Mobile Apps.
Win-Back Campaign Deep Links
async function sendWinBackCampaign(userId, segment) {
const deepLink = await Tolinku.createLink({
path: '/welcome-back',
params: {
source: 'win_back',
segment: segment.type,
offer: segment.offer,
},
});
const messages = {
moderate_lapse: {
push: { title: 'We have been busy!', body: '3 new features since you last visited.' },
email: { subject: 'See what is new in [App]' },
},
long_lapse: {
push: { title: 'We missed you!', body: 'Come back and see what has changed.' },
email: { subject: 'A lot has changed. Come take a look.' },
},
churned: {
push: { title: 'Your data is waiting', body: 'Your projects are still here. Come back anytime.' },
email: { subject: 'Your [App] account is still active' },
},
};
const msg = messages[segment.type];
// Send push first, email as follow-up
await sendPush(userId, {
...msg.push,
data: { deepLink: deepLink.url },
});
// Email 24h later if they don't return
scheduleEmail(userId, {
...msg.email,
deepLink: deepLink.url,
sendAt: addHours(24),
condition: () => userNotActive(userId, '24h'),
});
}
Handling Re-Engagement Deep Links
async function handleWelcomeBackDeepLink(params) {
const user = await getCurrentUser();
// Track the re-engagement
analytics.track('re_engagement_deep_link_opened', {
segment: params.segment,
source: params.source,
daysSinceLastActive: daysSince(user.lastActiveAt),
});
// Apply any offer
if (params.offer) {
await applyReEngagementOffer(user.id, params.offer);
}
// Route to appropriate re-onboarding
const segment = detectReturningUser(user.id);
if (segment.needsReOnboarding) {
navigation.navigate('ReOnboarding', { level: segment.level });
} else {
navigation.navigate('Home');
}
}
Content-Specific Win-Back Links
Bring users back with specific content relevant to their interests:
async function generatePersonalizedWinBackLink(userId) {
const user = await getUser(userId);
const interests = user.interests || [];
const newContent = await getNewContentForInterests(interests, user.lastActiveAt);
if (newContent.length > 0) {
const topContent = newContent[0];
return await Tolinku.createLink({
path: `/content/${topContent.id}`,
params: {
source: 'win_back',
content_type: topContent.type,
},
ogTitle: `New in [App]: ${topContent.title}`,
ogDescription: topContent.preview,
});
}
// Fallback to generic win-back
return await Tolinku.createLink({
path: '/welcome-back',
params: { source: 'win_back' },
});
}
Re-Onboarding Offers
Incentives by Segment
| Segment | Offer Type | Example |
|---|---|---|
| Moderate lapse | Feature unlock | "Try our new Premium feature free for 7 days" |
| Long lapse | Discount | "Welcome back! 30% off Premium this month" |
| Churned | Extended trial | "Start a new 14-day free trial" |
| Churned (former paying) | Win-back discount | "Get 50% off for 3 months" |
Offer Implementation
async function applyReEngagementOffer(userId, offerCode) {
const offers = {
COMEBACK_TRIAL: {
type: 'trial_extension',
days: 7,
features: ['premium'],
},
COMEBACK_30: {
type: 'discount',
percent: 30,
duration: '1_month',
},
COMEBACK_50: {
type: 'discount',
percent: 50,
duration: '3_months',
minAbsenceDays: 90,
},
};
const offer = offers[offerCode];
if (offer === undefined) return;
// Validate eligibility
const user = await getUser(userId);
if (offer.minAbsenceDays) {
const absence = daysSince(user.lastActiveAt);
if (absence < offer.minAbsenceDays) return;
}
await applyOffer(userId, offer);
await notifyUser(userId, `Welcome back! Your ${offer.type} has been applied.`);
}
Account State Management
Handling Changed Data
Things that may have changed while the user was away:
async function reconcileReturningUser(user) {
const changes = [];
// Check if any connected accounts expired
const connectedAccounts = await getConnectedAccounts(user.id);
for (const account of connectedAccounts) {
if (account.tokenExpired) {
changes.push({
type: 'reconnect_required',
account: account.name,
action: () => navigation.navigate('ReconnectAccount', { accountId: account.id }),
});
}
}
// Check if subscription lapsed
if (user.subscriptionStatus === 'expired') {
changes.push({
type: 'subscription_expired',
action: () => navigation.navigate('Resubscribe'),
});
}
// Check for pending notifications
const unread = await getUnreadNotifications(user.id);
if (unread.length > 0) {
changes.push({
type: 'pending_notifications',
count: unread.length,
action: () => navigation.navigate('Notifications'),
});
}
return changes;
}
Measuring Re-Onboarding
Key Metrics
| Metric | Definition | Target |
|---|---|---|
| Win-back rate | Returned users / Win-back messages sent | 5-15% |
| Re-activation rate | Active D7 after return / Returned users | 30-50% |
| Re-onboarding completion | Completed / Shown re-onboarding | 70-90% |
| Re-churn rate | Churned again within 30 days / Returned | < 50% |
| Revenue recovery | Revenue from returned users / Win-back cost | > 3x |
Cohort Tracking
async function reOnboardingCohortAnalysis() {
const segments = ['moderate_lapse', 'long_lapse', 'churned'];
for (const segment of segments) {
const returned = await getReturnedUsers(segment, { last: '90d' });
const reActivated = returned.filter(u => u.activeDay7AfterReturn);
const reChurned = returned.filter(u => u.churnedWithin30Days);
console.log(segment, {
returned: returned.length,
reActivationRate: (reActivated.length / returned.length * 100).toFixed(1) + '%',
reChurnRate: (reChurned.length / returned.length * 100).toFixed(1) + '%',
});
}
}
If the re-churn rate is above 50%, the re-onboarding isn't addressing why users left in the first place. Investigate the original churn reasons before optimizing the re-onboarding flow. For proactive strategies on preventing churn before it happens, see Reducing App Churn: Strategies That Actually Work.
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.