Invited users are different from organic users. They already have context: someone they know (or a team they belong to) sent them a link with a specific purpose. The onboarding flow should reflect that context instead of treating them like a stranger discovering your app for the first time.
For personalization strategies, see Personalized Onboarding Flows with Deep Link Data. For deferred deep linking mechanics, see Deferred Deep Linking: How It Works.
Types of Invite Links
Different invitation types require different onboarding flows:
| Invite Type | Context | Onboarding Goal |
|---|---|---|
| Team invite | Workspace name, role, inviter | Join the team, skip product education |
| Friend referral | Referrer name, reward | Create account, claim reward (see How Referral Deep Links Work) |
| Content share | Shared item (photo, playlist, doc) | View shared content, then sign up |
| Collaboration invite | Project, task, or document | Access the shared item, contribute |
| Event invite | Event details, date, host | RSVP, add to calendar |
Implementation Pattern
Step 1: Generate the Invite Link
async function createInviteLink(inviter, inviteType, metadata) {
const link = await Tolinku.createLink({
path: `/invite/${inviteType}`,
params: {
inviter_id: inviter.id,
inviter_name: inviter.firstName,
type: inviteType,
...metadata, // team_id, project_id, event_id, etc.
},
ogTitle: `${inviter.firstName} invited you to ${getInviteTitle(inviteType, metadata)}`,
ogDescription: getInviteDescription(inviteType, metadata),
});
return link.url;
}
// Team invite
const teamLink = await createInviteLink(user, 'team', {
team_id: team.id,
team_name: team.name,
role: 'member',
});
// Content share
const shareLink = await createInviteLink(user, 'share', {
content_type: 'playlist',
content_id: playlist.id,
content_name: playlist.name,
});
Step 2: Capture Invite Context on First Launch
async function handleFirstLaunch() {
const deferred = await Tolinku.checkDeferredLink();
if (deferred && deferred.path.startsWith('/invite/')) {
const params = deferred.params;
inviteStore.set({
type: params.type,
inviterId: params.inviter_id,
inviterName: params.inviter_name,
teamId: params.team_id,
contentId: params.content_id,
timestamp: Date.now(),
});
return navigateToInviteOnboarding(params.type);
}
return navigateToDefaultOnboarding();
}
Step 3: Route to the Right Flow
function navigateToInviteOnboarding(inviteType) {
switch (inviteType) {
case 'team':
return navigation.navigate('TeamInviteOnboarding');
case 'share':
return navigation.navigate('SharedContentView');
case 'referral':
return navigation.navigate('ReferralWelcome');
case 'collab':
return navigation.navigate('CollabOnboarding');
default:
return navigation.navigate('DefaultOnboarding');
}
}
Team Invite Onboarding
Team invitations are the most common invite type for B2B and collaboration apps. The invitee already knows the product's purpose (their teammate uses it), so the onboarding should focus on getting them into the workspace quickly.
Flow Design
Standard onboarding (6 steps):
- Welcome screen
- Feature tour
- Account creation
- Profile setup
- Preferences
- Dashboard
Team invite onboarding (3 steps):
- "Alex invited you to join Design Team" (with team context)
- Account creation (email pre-filled if included in invite)
- Team workspace (already joined)
You can cut the flow in half because the invitation provides the context that the standard flow tries to establish.
Implementation
function TeamInviteOnboarding({ invite }) {
const [step, setStep] = useState(0);
const steps = [
<TeamWelcome
inviterName={invite.inviterName}
teamName={invite.teamName}
role={invite.role}
/>,
<AccountCreation
prefillEmail={invite.email}
teamContext={invite.teamName}
/>,
// Skip feature tour, preferences, etc.
// User lands directly in the team workspace
];
return (
<OnboardingContainer
steps={steps}
currentStep={step}
onNext={() => {
if (step === steps.length - 1) {
joinTeamAndNavigate(invite.teamId);
} else {
setStep(step + 1);
}
}}
/>
);
}
async function joinTeamAndNavigate(teamId) {
await api.joinTeam(teamId);
navigation.reset({ routes: [{ name: 'TeamWorkspace', params: { teamId } }] });
}
Auto-Join vs. Approval
Decide whether invited users join automatically or need approval:
async function processTeamJoin(userId, invite) {
const team = await getTeam(invite.teamId);
if (team.autoApproveInvites) {
// Direct join
await addTeamMember(team.id, userId, invite.role);
return { status: 'joined', teamId: team.id };
}
// Approval required
await createJoinRequest(team.id, userId, invite.inviterId);
return { status: 'pending_approval', teamId: team.id };
}
Content Share Onboarding
When someone shares content (a photo album, playlist, document, or product), the recipient's primary intent is to view that content. Account creation is secondary.
Show Content First, Ask for Account Later
function SharedContentOnboarding({ invite }) {
const [hasViewed, setHasViewed] = useState(false);
if (hasViewed === false) {
return (
<ContentPreview
contentType={invite.contentType}
contentId={invite.contentId}
sharedBy={invite.inviterName}
onView={() => setHasViewed(true)}
onSignUp={() => navigation.navigate('QuickSignUp')}
/>
);
}
return (
<ContentEngagement
prompt={`Want to save this ${invite.contentType}? Create a free account.`}
onSignUp={() => navigation.navigate('QuickSignUp', { returnTo: invite.contentId })}
onSkip={() => navigation.navigate('Browse')}
/>
);
}
The key principle: let them see what was shared before asking for anything. A user who sees value in the content is far more likely to create an account than one who hits a signup wall immediately.
Gated vs. Ungated Content
| Content Type | Show Without Account? | Why |
|---|---|---|
| Public post/photo | Yes | Low barrier, builds trust |
| Playlist/collection | Preview (first 3 items) | Tease value, encourage signup |
| Document/file | Title + preview | Security concern if fully open |
| Private group content | No (require signup) | Privacy of group members |
| Paid content | Teaser only | Business model requires account |
Acknowledge the Inviter
Invited users arrive with a social connection. Use it throughout onboarding.
Welcome Screen Copy
Instead of a generic "Welcome to [App]," reference the invitation:
function getWelcomeMessage(invite) {
switch (invite.type) {
case 'team':
return {
headline: `${invite.inviterName} invited you to ${invite.teamName}`,
subtext: `Join your team on [App] to collaborate in real time.`,
};
case 'share':
return {
headline: `${invite.inviterName} shared something with you`,
subtext: `Open [App] to view it.`,
};
case 'referral':
return {
headline: `${invite.inviterName} thinks you'll love [App]`,
subtext: `Sign up and you both get a reward.`,
};
default:
return {
headline: `You've been invited to [App]`,
subtext: `Create your account to get started.`,
};
}
}
Social Proof Throughout
Show the inviter's presence during onboarding steps:
- Account creation: "Join Alex and 12 others on Design Team"
- Feature tour: "Alex uses [Feature] to manage design files"
- Completion: "You're all set. Alex has been notified."
Small touches that reinforce the social connection reduce drop-off because the user feels expected, not anonymous.
Pre-Filling Invite Data
What to Pre-Fill
| Data Source | Pre-Fill Field | User Action |
|---|---|---|
| Email invite | Email address | Confirm (locked) |
| Team invite | Team name, role | Auto-set |
| Calendar invite | Event date/time | Auto-add |
| Partner invite | Organization | Auto-associate |
Implementation
function InviteAccountCreation({ invite }) {
return (
<Form onSubmit={handleSignUp}>
{invite.email && (
<Input
label="Email"
value={invite.email}
disabled={true}
helperText="Invitation was sent to this email"
/>
)}
<Input label="Password" type="password" />
<Input
label="Name"
defaultValue={invite.recipientName || ''}
/>
{invite.teamName && (
<InfoBox>
You'll be added to <strong>{invite.teamName}</strong> as a {invite.role}.
</InfoBox>
)}
<Button type="submit">
{invite.type === 'team' ? 'Join Team' : 'Create Account'}
</Button>
</Form>
);
}
Invite Expiration and Validation
Expiration Rules
Invite links should expire. The appropriate duration depends on the invite type:
| Invite Type | Expiration | Reason |
|---|---|---|
| Team invite | 7-14 days | Security; team membership should be timely |
| Content share | 30 days | Content may become irrelevant |
| Event invite | Event date + 1 day | No point after the event |
| Referral | 30-90 days | Marketing window |
Validation Flow
async function validateInvite(inviteParams) {
// Check if invite exists and hasn't been used
const invite = await getInvite(inviteParams.inviter_id, inviteParams.type);
if (invite === null) {
return { valid: false, reason: 'not_found' };
}
if (invite.usedAt) {
return { valid: false, reason: 'already_used' };
}
if (invite.expiresAt < Date.now()) {
return { valid: false, reason: 'expired' };
}
// For team invites, check if team still exists
if (inviteParams.type === 'team') {
const team = await getTeam(inviteParams.team_id);
if (team === null) {
return { valid: false, reason: 'team_deleted' };
}
}
return { valid: true, invite };
}
Handling Invalid Invites
When an invite link is expired or invalid, don't just show an error. Offer alternatives:
function InvalidInviteScreen({ reason, invite }) {
const messages = {
expired: {
title: 'This invite has expired',
action: `Ask ${invite.inviterName} to send a new invite.`,
},
already_used: {
title: 'This invite has already been used',
action: 'Log in to access your account.',
},
team_deleted: {
title: 'This team no longer exists',
action: 'Create your own account to get started.',
},
not_found: {
title: 'Invite not found',
action: 'The link may be incorrect. Ask the sender for a new one.',
},
};
const msg = messages[reason];
return (
<Screen>
<Heading>{msg.title}</Heading>
<Text>{msg.action}</Text>
<Button onPress={() => navigation.navigate('SignUp')}>
Create Account
</Button>
</Screen>
);
}
Measuring Invite Onboarding
Key Metrics
| Metric | What It Tells You |
|---|---|
| Invite click-to-install rate | How compelling the invite preview is |
| Invite onboarding completion | Whether the invited flow works |
| Time to first action | How quickly invited users become active |
| Invite vs. organic retention (Day 7) | Whether invited users stick around longer |
| Inviter notification open rate | Whether the inviter re-engages when their friend joins |
Tracking
analytics.track('invite_onboarding', {
step: 'account_created',
inviteType: invite.type,
inviterId: invite.inviterId,
stepsShown: 3, // vs. 6 for standard
timeToComplete: Date.now() - invite.startedAt,
});
Compare invited user metrics against organic signups to validate that the abbreviated onboarding outperforms the standard flow for this segment.
For onboarding use cases, see the onboarding documentation. For referral features, see Tolinku referrals. For referral program setup, see the referral docs.
Get deep linking tips in your inbox
One email per week. No spam.