Introduction
First impressions matter in SaaS. A smooth, automated onboarding experience can mean the difference between a loyal customer and a churned trial user. Studies show that 86% of users are more likely to stay loyal to a business that provides welcoming onboarding content after they've made a purchase.
Laravel 12 provides powerful tools to create seamless, automated onboarding flows that guide new users from registration to their first success milestone. From email verification and welcome sequences to progressive profile completion and in-app tours, this guide covers everything you need to build a production-ready onboarding system.
Whether you're building a new SaaS or improving an existing one, automating your onboarding process will reduce support tickets, increase activation rates, and improve user retention from day one.
Understanding SaaS Onboarding Goals
Before diving into code, let's understand what makes effective SaaS onboarding and what you should aim to achieve.
Key Onboarding Objectives
User Activation
- Get users to their "aha moment" quickly
- Complete essential profile information
- Perform first meaningful action in your app
- Understand core value proposition
Reduce Time to Value
- Guide users to first success milestone
- Minimize steps between signup and productivity
- Provide contextual help at the right time
- Remove friction from initial setup
Build User Confidence
- Clear progress indicators
- Celebrate small wins
- Provide immediate support options
- Set appropriate expectations
Collect Necessary Data
- Progressive profile completion
- Understand user goals and use cases
- Gather preferences without overwhelming
- Enable personalized experiences
Common Onboarding Flow Stages
- Account Creation - Registration with email verification
- Profile Setup - Basic information and preferences
- Welcome Tour - Introduction to key features
- First Action - Complete initial task or project
- Follow-up - Automated emails to drive engagement
Building the Registration System
Let's start with a robust registration system that includes email verification and initial user setup.
Enhanced Registration Controller
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Jobs\SendWelcomeEmail;
use App\Jobs\InitializeUserWorkspace;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class RegisterController extends Controller
{
/**
* Handle user registration
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
'company_name' => ['nullable', 'string', 'max:255'],
'use_case' => ['nullable', 'string', 'in:project_management,crm,analytics,other'],
'team_size' => ['nullable', 'string', 'in:1-5,6-20,21-50,51+'],
]);
// Create user
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'onboarding_step' => 'email_verification',
]);
// Store additional onboarding data
$user->onboarding_data = [
'company_name' => $validated['company_name'] ?? null,
'use_case' => $validated['use_case'] ?? null,
'team_size' => $validated['team_size'] ?? null,
'registered_at' => now()->toIso8601String(),
];
$user->save();
// Fire registered event (triggers email verification)
event(new Registered($user));
// Queue welcome email
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
// Initialize workspace in background
InitializeUserWorkspace::dispatch($user);
// Log in the user
auth()->login($user);
return redirect()->route('onboarding.verify-email')
->with('success', 'Account created! Please verify your email.');
}
}
Email Verification Flow
namespace App\Http\Controllers\Onboarding;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Auth\Events\Verified;
class EmailVerificationController extends Controller
{
/**
* Show email verification notice
*/
public function show()
{
if (auth()->user()->hasVerifiedEmail()) {
return redirect()->route('onboarding.profile-setup');
}
return view('onboarding.verify-email');
}
/**
* Verify email address
*/
public function verify(Request $request, $id, $hash)
{
$user = User::findOrFail($id);
if (!hash_equals((string) $hash, sha1($user->getEmailForVerification()))) {
abort(403, 'Invalid verification link');
}
if ($user->hasVerifiedEmail()) {
return redirect()->route('onboarding.profile-setup')
->with('message', 'Email already verified');
}
if ($user->markEmailAsVerified()) {
event(new Verified($user));
// Update onboarding progress
$user->update(['onboarding_step' => 'profile_setup']);
}
return redirect()->route('onboarding.profile-setup')
->with('success', 'Email verified successfully!');
}
/**
* Resend verification email
*/
public function resend(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->route('onboarding.profile-setup');
}
$request->user()->sendEmailVerificationNotification();
return back()->with('success', 'Verification email sent!');
}
}
Progressive Profile Completion
Guide users through profile setup without overwhelming them. Collect information progressively across multiple steps.
Multi-Step Profile Setup
namespace App\Http\Controllers\Onboarding;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProfileSetupController extends Controller
{
/**
* Show profile setup wizard
*/
public function show()
{
$user = auth()->user();
return view('onboarding.profile-setup', [
'user' => $user,
'currentStep' => $user->onboarding_step,
'completionPercentage' => $this->calculateCompletion($user),
]);
}
/**
* Update profile information
*/
public function update(Request $request)
{
$user = auth()->user();
$step = $request->input('step');
match($step) {
'personal_info' => $this->updatePersonalInfo($request, $user),
'company_info' => $this->updateCompanyInfo($request, $user),
'preferences' => $this->updatePreferences($request, $user),
default => abort(400, 'Invalid step'),
};
// Update onboarding progress
$nextStep = $this->getNextStep($step);
$user->update(['onboarding_step' => $nextStep]);
// If onboarding complete, mark as such
if ($nextStep === 'completed') {
$user->update([
'onboarding_completed_at' => now(),
'onboarding_step' => 'completed',
]);
return redirect()->route('dashboard')
->with('success', 'Welcome! Your account is all set up.');
}
return back()->with('success', 'Progress saved!');
}
/**
* Update personal information
*/
protected function updatePersonalInfo(Request $request, User $user): void
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'phone' => ['nullable', 'string', 'max:20'],
'timezone' => ['required', 'string', 'timezone'],
'avatar' => ['nullable', 'image', 'max:2048'],
]);
if ($request->hasFile('avatar')) {
// Delete old avatar
if ($user->avatar) {
Storage::delete($user->avatar);
}
$validated['avatar'] = $request->file('avatar')
->store('avatars', 'public');
}
$user->update($validated);
}
/**
* Update company information
*/
protected function updateCompanyInfo(Request $request, User $user): void
{
$validated = $request->validate([
'company_name' => ['required', 'string', 'max:255'],
'company_size' => ['required', 'string', 'in:1-10,11-50,51-200,201+'],
'industry' => ['required', 'string', 'max:100'],
'role' => ['required', 'string', 'max:100'],
]);
$onboardingData = $user->onboarding_data ?? [];
$user->onboarding_data = array_merge($onboardingData, $validated);
$user->save();
}
/**
* Update user preferences
*/
protected function updatePreferences(Request $request, User $user): void
{
$validated = $request->validate([
'email_notifications' => ['boolean'],
'marketing_emails' => ['boolean'],
'language' => ['required', 'string', 'in:en,es,fr,de'],
'date_format' => ['required', 'string', 'in:Y-m-d,m/d/Y,d/m/Y'],
]);
$user->preferences = $validated;
$user->save();
}
/**
* Calculate profile completion percentage
*/
protected function calculateCompletion(User $user): int
{
$fields = [
'name' => $user->name,
'email_verified' => $user->hasVerifiedEmail(),
'phone' => $user->phone,
'avatar' => $user->avatar,
'company_name' => $user->onboarding_data['company_name'] ?? null,
'timezone' => $user->timezone,
'preferences' => !empty($user->preferences),
];
$completed = count(array_filter($fields));
$total = count($fields);
return (int) (($completed / $total) * 100);
}
/**
* Get next onboarding step
*/
protected function getNextStep(string $currentStep): string
{
$steps = [
'personal_info' => 'company_info',
'company_info' => 'preferences',
'preferences' => 'welcome_tour',
'welcome_tour' => 'completed',
];
return $steps[$currentStep] ?? 'completed';
}
}
Automated Welcome Email Sequence
Create a drip campaign that guides users through their first week with your application.
Welcome Email Job
namespace App\Jobs;
use App\Models\User;
use App\Mail\WelcomeEmail;
use App\Mail\OnboardingTip;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public User $user
) {}
public function handle(): void
{
// Send immediate welcome email
Mail::to($this->user)->send(new WelcomeEmail($this->user));
// Schedule follow-up emails
$this->scheduleFollowUpEmails();
}
/**
* Schedule drip email sequence
*/
protected function scheduleFollowUpEmails(): void
{
// Day 1: Getting started tips
SendOnboardingTip::dispatch($this->user, 'getting_started')
->delay(now()->addDay());
// Day 3: Feature highlight
SendOnboardingTip::dispatch($this->user, 'key_features')
->delay(now()->addDays(3));
// Day 7: Success stories
SendOnboardingTip::dispatch($this->user, 'success_stories')
->delay(now()->addDays(7));
// Day 14: Check-in email
SendOnboardingTip::dispatch($this->user, 'check_in')
->delay(now()->addDays(14));
}
}
Onboarding Tip Email Job
namespace App\Jobs;
use App\Models\User;
use App\Mail\OnboardingTip;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendOnboardingTip implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public User $user,
public string $tipType
) {}
public function handle(): void
{
// Only send if user hasn't completed onboarding or cancelled
if ($this->user->onboarding_step === 'completed' ||
$this->user->onboarding_emails_cancelled) {
return;
}
Mail::to($this->user)->send(
new OnboardingTip($this->user, $this->tipType)
);
// Track email sent
$this->user->increment('onboarding_emails_sent');
}
}
Welcome Email Mailable
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public User $user
) {}
public function build()
{
return $this->subject('Welcome to ' . config('app.name') . '!')
->markdown('emails.onboarding.welcome', [
'user' => $this->user,
'dashboardUrl' => route('dashboard'),
'supportUrl' => route('support'),
'tutorialsUrl' => route('tutorials'),
]);
}
}
{{-- resources/views/emails/onboarding/welcome.blade.php --}}
@component('mail::message')
# Welcome, {{ $user->name }}! 🎉
Thank you for joining {{ config('app.name') }}. We're excited to help you achieve your goals!
## What's Next?
Here are some quick steps to get you started:
@component('mail::panel')
**1. Complete Your Profile**
Add your details to personalize your experience.
**2. Explore Key Features**
Check out our most popular tools and features.
**3. Create Your First Project**
Jump right in and see how easy it is to get started.
@endcomponent
@component('mail::button', ['url' => $dashboardUrl])
Go to Dashboard
@endcomponent
## Need Help?
Our support team is here for you:
- Check out our [tutorials]({{ $tutorialsUrl }})
- Contact [support]({{ $supportUrl }})
- Join our community forum
Welcome aboard!
Thanks,<br>
The {{ config('app.name') }} Team
@endcomponent
In-App Onboarding Tour
Guide users through your application's key features with an interactive tour.
Tour Configuration
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class OnboardingTourService
{
/**
* Get tour steps for user
*/
public function getTourSteps(User $user): array
{
return [
[
'id' => 'dashboard',
'title' => 'Your Dashboard',
'description' => 'This is your command center. View your projects, tasks, and team activity here.',
'element' => '#dashboard-overview',
'position' => 'bottom',
],
[
'id' => 'create_project',
'title' => 'Create Projects',
'description' => 'Click here to create your first project and start organizing your work.',
'element' => '#create-project-btn',
'position' => 'left',
],
[
'id' => 'team',
'title' => 'Invite Your Team',
'description' => 'Collaboration is better with teammates. Invite your team to join.',
'element' => '#invite-team-btn',
'position' => 'bottom',
],
[
'id' => 'notifications',
'title' => 'Stay Updated',
'description' => 'Get real-time notifications about project updates and team activities.',
'element' => '#notifications-icon',
'position' => 'left',
],
[
'id' => 'settings',
'title' => 'Customize Settings',
'description' => 'Personalize your workspace with custom settings and preferences.',
'element' => '#settings-link',
'position' => 'left',
],
];
}
/**
* Mark tour as completed
*/
public function completeTour(User $user, string $tourName = 'main'): void
{
$completedTours = $user->completed_tours ?? [];
$completedTours[$tourName] = now()->toIso8601String();
$user->completed_tours = $completedTours;
$user->save();
// Update onboarding step if this was first tour
if ($user->onboarding_step === 'welcome_tour') {
$user->update(['onboarding_step' => 'completed']);
}
}
/**
* Check if user has completed tour
*/
public function hasTourCompleted(User $user, string $tourName = 'main'): bool
{
return isset($user->completed_tours[$tourName]);
}
/**
* Skip tour
*/
public function skipTour(User $user, string $tourName = 'main'): void
{
$skippedTours = $user->skipped_tours ?? [];
$skippedTours[$tourName] = now()->toIso8601String();
$user->skipped_tours = $skippedTours;
$user->save();
// Still mark onboarding as complete
if ($user->onboarding_step === 'welcome_tour') {
$user->update(['onboarding_step' => 'completed']);
}
}
}
Tour Controller
namespace App\Http\Controllers\Onboarding;
use App\Http\Controllers\Controller;
use App\Services\OnboardingTourService;
use Illuminate\Http\Request;
class TourController extends Controller
{
public function __construct(
protected OnboardingTourService $tourService
) {}
/**
* Get tour steps
*/
public function steps(Request $request)
{
$user = $request->user();
return response()->json([
'steps' => $this->tourService->getTourSteps($user),
'completed' => $this->tourService->hasTourCompleted($user),
]);
}
/**
* Complete tour
*/
public function complete(Request $request)
{
$this->tourService->completeTour($request->user());
return response()->json([
'message' => 'Tour completed',
]);
}
/**
* Skip tour
*/
public function skip(Request $request)
{
$this->tourService->skipTour($request->user());
return response()->json([
'message' => 'Tour skipped',
]);
}
}
Frontend Tour Integration (Alpine.js)
// resources/js/components/onboarding-tour.js
document.addEventListener('alpine:init', () => {
Alpine.data('onboardingTour', () => ({
active: false,
currentStep: 0,
steps: [],
async init() {
// Fetch tour steps
const response = await fetch('/api/onboarding/tour/steps');
const data = await response.json();
if (!data.completed) {
this.steps = data.steps;
this.active = true;
this.highlightElement();
}
},
nextStep() {
if (this.currentStep < this.steps.length - 1) {
this.currentStep++;
this.highlightElement();
} else {
this.completeTour();
}
},
previousStep() {
if (this.currentStep > 0) {
this.currentStep--;
this.highlightElement();
}
},
async completeTour() {
await fetch('/api/onboarding/tour/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
this.active = false;
this.cleanup();
},
async skipTour() {
await fetch('/api/onboarding/tour/skip', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
this.active = false;
this.cleanup();
},
highlightElement() {
this.cleanup();
const step = this.steps[this.currentStep];
const element = document.querySelector(step.element);
if (element) {
element.classList.add('tour-highlight');
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
},
cleanup() {
document.querySelectorAll('.tour-highlight').forEach(el => {
el.classList.remove('tour-highlight');
});
}
}));
});
Tracking Onboarding Progress
Monitor user progress through the onboarding funnel to identify drop-off points.
Onboarding Analytics Service
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class OnboardingAnalyticsService
{
/**
* Get onboarding funnel metrics
*/
public function getFunnelMetrics(): array
{
$total = User::count();
return [
'total_users' => $total,
'email_verified' => [
'count' => User::whereNotNull('email_verified_at')->count(),
'percentage' => $this->percentage(
User::whereNotNull('email_verified_at')->count(),
$total
),
],
'profile_completed' => [
'count' => User::where('onboarding_step', '!=', 'email_verification')->count(),
'percentage' => $this->percentage(
User::where('onboarding_step', '!=', 'email_verification')->count(),
$total
),
],
'tour_completed' => [
'count' => User::whereNotNull('completed_tours')->count(),
'percentage' => $this->percentage(
User::whereNotNull('completed_tours')->count(),
$total
),
],
'onboarding_completed' => [
'count' => User::whereNotNull('onboarding_completed_at')->count(),
'percentage' => $this->percentage(
User::whereNotNull('onboarding_completed_at')->count(),
$total
),
],
];
}
/**
* Get average time to complete onboarding
*/
public function getAverageCompletionTime(): float
{
$avg = User::whereNotNull('onboarding_completed_at')
->selectRaw('AVG(TIMESTAMPDIFF(HOUR, created_at, onboarding_completed_at)) as avg_hours')
->value('avg_hours');
return round($avg ?? 0, 2);
}
/**
* Get drop-off points
*/
public function getDropOffPoints(): array
{
return DB::table('users')
->select('onboarding_step', DB::raw('COUNT(*) as count'))
->groupBy('onboarding_step')
->get()
->mapWithKeys(fn($item) => [$item->onboarding_step => $item->count])
->toArray();
}
/**
* Calculate percentage
*/
protected function percentage(int $part, int $total): float
{
return $total > 0 ? round(($part / $total) * 100, 2) : 0;
}
}
Real-World Implementation Example
At NeedLaravelSite, we implemented this automated onboarding system for a project management SaaS:
Results After 3 Months:
- 87% email verification rate (up from 62%)
- 73% profile completion rate (up from 45%)
- 65% tour completion rate
- 58% full onboarding completion (up from 31%)
- 40% reduction in support tickets from new users
- Average onboarding time: 12 minutes (down from 35 minutes)
Key Success Factors:
- Progressive disclosure - not overwhelming users
- Clear value communication at each step
- Immediate wins (sample data, quick tutorials)
- Follow-up emails at optimal intervals
- Easy skip options (no forced engagement)
Conclusion
Automating SaaS user onboarding with Laravel 12 dramatically improves user activation and retention. By guiding users from registration to their first success, you create a positive first impression that drives long-term engagement.
Key Takeaways:
Implement email verification and welcome sequences to engage users immediately after registration with personalized content and clear next steps.
Use progressive profile completion to collect necessary information without overwhelming users, breaking setup into digestible steps with clear progress indicators.
Create interactive in-app tours that highlight key features and guide users to their first success milestone within minutes of signing up.
Track onboarding metrics religiously to identify drop-off points, measure completion rates, and continuously optimize your funnel for better activation.
Automate follow-up communications with drip email campaigns that provide value, encourage engagement, and offer support at critical moments in the user journey.
Need Expert Help with Your Laravel SaaS?
At NeedLaravelSite, we specialize in building seamless onboarding experiences that convert trial users into loyal customers. With 8+ years of Laravel expertise, we can help you:
- Design and implement automated onboarding flows
- Create engaging email sequences and in-app tours
- Build analytics dashboards to track onboarding metrics
- Optimize your funnel for maximum activation rates
Get in touch today and let's create an onboarding experience that delights your users.