A feature-rich onboarding, checklist, journey, progression, and gated-experience engine for Laravel.
Passage lets applications define multi-step journeys in code, enroll any Eloquent model, persist step progress, enforce prerequisites, calculate completion, automatically evaluate conditions, redirect users to their next task, send reminders, record audit history, expire overdue journeys, and safely restart repeatable flows.
- Fluent and configuration-based passage definitions
- Required and optional steps
- Step prerequisites and dependency chains
- Automatic completion conditions
- Conditional step visibility
- Named-route and direct-URL destinations
- Persistent enrollment and step progress
- Pending, in-progress, blocked, completed, expired, and cancelled enrollment states
- Pending, in-progress, completed, skipped, failed, and blocked step states
- Retry policies and maximum-attempt limits
- Passage and step deadlines
- Repeatable passages with cycle tracking
- Manual completion, administrative overrides, skipping, failure, cancellation, and restart
- Progress percentages and next-step resolution
- Middleware for passage and individual-step gating
- JSON-friendly incomplete responses
- Events for every major lifecycle transition
- Queued mail/database-compatible reminders
- Audit history with optional actor attribution
- Metadata on definitions, enrollments, and steps
- Automatic repair and synchronization commands
- Expiration and retention-pruning commands
- Configurable models and table names
- Integer and UUID/string subject-key support
- Laravel package auto-discovery
- PHPUnit, PHPStan/Larastan, Pint, and GitHub Actions
| Package version | PHP | Laravel / Illuminate |
|---|---|---|
| Current | ^8.2 |
^12.0 || ^13.0 |
Composer automatically resolves compatible Laravel and Illuminate versions for the consuming application.
composer require eloquent-works/passage
php artisan passage:install --migrateAdd HasPassages to any Eloquent model that can participate:
use EloquentWorks\Passage\Traits\HasPassages;
class User extends Authenticatable
{
use HasPassages;
}Register passages during application boot, such as in AppServiceProvider::boot():
use App\Passage\Conditions\EmailIsVerified;
use EloquentWorks\Passage\Definitions\StepDefinition;
use EloquentWorks\Passage\Facades\Passage;
Passage::define('account-setup')
->name('Account setup')
->description('Finish the required account setup tasks.')
->category('onboarding')
->version(1)
->dueAfterMinutes(7 * 24 * 60)
->tags('onboarding', 'account')
->step('verify-email', function (StepDefinition $step): void {
$step
->name('Verify your email')
->route('verification.notice')
->completeWhen(EmailIsVerified::class);
})
->step('complete-profile', function (StepDefinition $step): void {
$step
->name('Complete your profile')
->route('profile.edit')
->dependsOn('verify-email');
})
->step('product-tour', function (StepDefinition $step): void {
$step
->optional()
->route('tour.start');
});$enrollment = $user->startPassage('account-setup', [
'source' => 'registration',
]);
$progress = $user->passageProgress('account-setup');
$progress->percentage; // 0β100
$progress->nextStep;
$progress->state;$user->completePassageStep('account-setup', 'verify-email');
$user->skipPassageStep('account-setup', 'product-tour');
$user->failPassageStep('account-setup', 'complete-profile', 'Validation failed');
$user->restartPassage('account-setup');Administrative override:
Passage::completeStep(
subject: $user,
passage: 'account-setup',
step: 'complete-profile',
actor: $administrator,
force: true,
);Conditions implement StepCondition:
use EloquentWorks\Passage\Contracts\StepCondition;
use EloquentWorks\Passage\Models\PassageEnrollment;
use EloquentWorks\Passage\Models\PassageStepProgress;
use Illuminate\Database\Eloquent\Model;
final class EmailIsVerified implements StepCondition
{
public function evaluate(
Model $subject,
PassageEnrollment $enrollment,
PassageStepProgress $step,
): bool {
return method_exists($subject, 'hasVerifiedEmail')
&& $subject->hasVerifiedEmail();
}
}Then synchronize:
Passage::sync($user, 'account-setup');Closures are also supported for definitions registered in code:
$step->completeWhen(
fn (User $user): bool => $user->profile?->isComplete() === true,
);Require an entire passage:
Route::get('/dashboard', DashboardController::class)
->middleware(['auth', 'passage.complete:account-setup']);Require one step:
Route::get('/advanced', AdvancedController::class)
->middleware(['auth', 'passage.step:account-setup,verify-email']);Redirect directly to the next configured step:
Route::get('/onboarding', OnboardingController::class)
->middleware(['auth', 'passage.next:account-setup']);JSON requests receive a configurable 409 response containing the progress snapshot.
php artisan passage:sync
php artisan passage:expire
php artisan passage:remind
php artisan passage:repair
php artisan passage:prune --forceSuggested scheduler:
use Illuminate\Support\Facades\Schedule;
Schedule::command('passage:sync')->hourly();
Schedule::command('passage:expire')->everyFifteenMinutes();
Schedule::command('passage:remind')->hourly();
Schedule::command('passage:prune')->daily();PassageEnrolledPassageCompletedPassageExpiredPassageCancelledPassageRestartedPassageReminderSentStepStartedStepCompletedStepSkippedStepFailed
composer validate --strict
composer qualityOr separately:
composer format
composer analyse
composer test- Installation
- Defining Passages
- Using Passage
- Conditions and Automation
- Middleware
- Commands and Scheduling
- Events and Audit History
- Configuration
- Testing
- Security
Passage tracks workflow state; it does not replace Laravel authorization. Continue to authorize protected actions with policies, gates, and application-specific checks. Treat enrollment and step metadata as user-generated data when it contains request input.
Report vulnerabilities privately according to SECURITY.md.
See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
Built by Eloquent Works.
Laravel Passage is open-source software licensed under the MIT License.