| name | level-up-development |
| description | Build and work with cjmellor/level-up features, including XP, levels, tiers, achievements, streaks, multipliers, leaderboards, and auditing. Use when this capability is needed. |
| metadata | {"author":"cjmellor"} |
When to use this skill
Use this skill when working with gamification features — adding experience points, levels, tiers, achievements, streaks, multipliers, leaderboards, or auditing — using cjmellor/level-up.
Core Concepts
- XP and Levels — Users earn experience points (XP) and automatically progress through a defined level structure.
- Tiers — Named status brackets (e.g. Bronze, Silver, Gold) based on XP thresholds, independent of numeric levels.
- Achievements — Unlockable rewards, optionally with progress tracking, optionally gated by tier.
- Streaks — Track consecutive daily activities with freeze support.
- Multipliers — Database-backed point modifiers with scoping, scheduling, and configurable stacking strategies.
- Leaderboard — Rank users by XP, optionally scoped to a tier.
- Auditing — Automatic history of all XP changes, level-ups, and tier changes.
Setup
Install
composer require cjmellor/level-up
php artisan vendor:publish --tag="level-up-migrations"
php artisan migrate
php artisan vendor:publish --tag="level-up-config"
Add Traits to the User Model
Add only the traits you need:
use LevelUp\Experience\Concerns\GiveExperience;
use LevelUp\Experience\Concerns\HasAchievements;
use LevelUp\Experience\Concerns\HasChallenges;
use LevelUp\Experience\Concerns\HasStreaks;
use LevelUp\Experience\Concerns\HasTiers;
class User extends Authenticatable
{
use GiveExperience;
use HasAchievements;
use HasStreaks;
use HasTiers;
use HasChallenges;
}
GiveExperience is the foundation. The others are opt-in.
Levels
Define Levels
use LevelUp\Experience\Models\Level;
Level::add(
['level' => 1, 'next_level_experience' => null],
['level' => 2, 'next_level_experience' => 100],
['level' => 3, 'next_level_experience' => 250],
['level' => 4, 'next_level_experience' => 500],
['level' => 5, 'next_level_experience' => 1000],
);
Level 1 must have next_level_experience set to null — it is the default starting point. Users level up automatically when their XP reaches the threshold. Throws LevelExistsException if a level number already exists.
Level Queries
$user->getLevel();
$user->getPoints();
$user->nextLevelAt();
$user->nextLevelAt(checkAgainst: 5);
$user->nextLevelAt(showAsPercentage: true);
Manual Level Up
$user->levelUp(to: 5);
Throws InvalidArgumentException if the level does not exist. Fires UserLevelledUp for each intermediate level gained. Respects the level cap.
Level Cap
Configured in config/level-up.php:
'level_cap' => [
'enabled' => env('LEVEL_CAP_ENABLED', true),
'level' => env('LEVEL_CAP', 100),
'points_continue' => env('LEVEL_CAP_POINTS_CONTINUE', true),
],
When the cap is reached, the user stops levelling. If points_continue is true, XP still accumulates. If false, XP stops accumulating too.
Experience Points (XP)
Add Points
$user->addPoints(50);
$user->addPoints(50, reason: 'Completed tutorial');
$user->addPoints(50, multiplier: 2);
$user->addPoints(50, type: AuditType::Add->value, reason: 'Bonus');
Creates an experience record if none exists, otherwise increments. Automatically levels up if the threshold is crossed. Throws if the amount exceeds the highest level's next_level_experience.
Deduct Points
$user->deductPoints(30);
$user->deductPoints(30, reason: 'Penalty');
Throws Exception if the user has no experience record.
Set Points
$user->setPoints(500);
Directly overwrites the XP total. Throws Exception if the user has no experience record.
Get Points
$user->getPoints();
Multipliers
Multipliers are database-backed records that modify point calculations. They can be scoped to specific users or tiers, scheduled with time windows, and configured to stack using different strategies.
Create Multipliers
use LevelUp\Experience\Models\Multiplier;
Multiplier::create([
'name' => 'Weekend Bonus',
'multiplier' => 2.0,
'is_active' => true,
'starts_at' => now()->startOfWeekend(),
'expires_at' => now()->endOfWeekend(),
]);
Active multipliers are automatically applied when addPoints() is called. The multiplier value must be at least 0.01. If both starts_at and expires_at are set, starts_at must be before expires_at.
Scope Multipliers
Multipliers with no scopes apply to all users. Use scopeTo() to restrict:
$multiplier->scopeTo($user);
$multiplier->scopeTo($goldTier);
$multiplier->scopeTo($user, $tier);
scopeTo() is idempotent — calling it twice with the same model does not create duplicates.
Inline Multiplier
$user->addPoints(amount: 10, multiplier: 3);
Inline multipliers stack with DB multipliers according to the configured strategy.
Stacking Strategies
Configure in config/level-up.php:
'multiplier' => [
'enabled' => env('MULTIPLIER_ENABLED', true),
'stack_strategy' => env('MULTIPLIER_STACK', 'compound'),
],
Query Multipliers
Multiplier::active()->get();
Multiplier::active()->forUser($user)->get();
Multiplier::scheduled()->get();
Multiplier::expired()->get();
MultiplierApplied Event
Fires when multipliers modify a point calculation. Properties: Model $user, Collection $multipliers, int $originalAmount, int $finalAmount, string $strategy.
Tiers
Define Tiers
use LevelUp\Experience\Models\Tier;
Tier::add(
['name' => 'Bronze', 'experience' => 0],
['name' => 'Silver', 'experience' => 500],
['name' => 'Gold', 'experience' => 2000],
['name' => 'Platinum', 'experience' => 5000, 'metadata' => ['color' => '#E5E4E2', 'icon' => 'crown']],
);
Tier names and experience values must be unique. Throws TierExistsException on duplicates. The metadata column is a flexible JSON field for any extra data (colours, icons, descriptions). The entire add() call is wrapped in a database transaction — if any tier fails, none are created.
Automatic Tier Promotion
Tiers update automatically when XP changes. When addPoints() causes the user to cross a tier threshold, experience.tier_id is updated and a UserTierUpdated event fires with TierDirection::Promoted.
Query Tiers
$user->getTier();
$user->getNextTier();
$user->tierProgress();
$user->nextTierAt();
$user->isAtTier('Gold');
$user->isAtOrAboveTier('Silver');
getTier() returns null if the user has no experience record or tiers are disabled.
Demotion
By default, tiers use a high-water mark — once earned, they persist even if points decrease. Enable demotion to allow tier drops:
TIER_DEMOTION=true
When enabled, deductPoints() checks if the user should drop and fires UserTierUpdated with TierDirection::Demoted. The newTier property is nullable — it will be null if the user drops below all tier thresholds.
Tier-Scoped Multipliers
Create a multiplier and scope it to a tier so it only applies to users at that tier:
$multiplier = Multiplier::create([
'name' => 'Gold Bonus',
'multiplier' => 2.0,
'is_active' => true,
]);
$goldTier = Tier::where('name', 'Gold')->first();
$multiplier->scopeTo($goldTier);
When a Gold-tier user calls addPoints(), this multiplier is automatically included.
Tier-Gated Achievements
Restrict achievements so only users at a certain tier can earn them:
$goldTier = Tier::where('name', 'Gold')->first();
Achievement::create([
'name' => 'Golden Streak',
'tier_id' => $goldTier->id,
]);
Attempting to grant to a user below Gold throws TierRequirementNotMet.
Tier-Scaled Streak Freezes
Higher tiers get longer freeze durations:
'tiers' => [
'streak_freeze_days' => [
'Bronze' => 1,
'Silver' => 2,
'Gold' => 3,
'Platinum' => 7,
],
],
Falls back to the global freeze_duration if the tier is not listed or tiers are disabled.
Tier-Scoped Leaderboards
use LevelUp\Experience\Facades\Leaderboard;
Leaderboard::forTier('Gold')->generate();
Leaderboard::forTier($tierModel)->generate();
Tier Config
'tiers' => [
'enabled' => env('TIERS_ENABLED', true),
'demotion' => env('TIER_DEMOTION', false),
'streak_freeze_days' => [],
],
Achievements
Create Achievements
use LevelUp\Experience\Models\Achievement;
Achievement::create([
'name' => 'First Login',
'is_secret' => false,
'description' => 'Log in for the first time',
'image' => 'storage/app/achievements/first-login.png',
]);
Achievement::create([
'name' => 'Hidden Gem',
'is_secret' => true,
]);
Achievement::create([
'name' => 'Gold Member Badge',
'tier_id' => $goldTier->id,
]);
Grant Achievement
$user->grantAchievement($achievement);
$user->grantAchievement($achievement, progress: 50);
Throws Exception if progress exceeds 100, or if the user already has the achievement. Throws TierRequirementNotMet if tier-gated and user does not meet the tier requirement. AchievementAwarded event fires only when progress is null or 100.
Revoke Achievement
$user->revokeAchievement($achievement);
Throws Exception if the user does not have the achievement.
Achievement Progress
$newProgress = $user->incrementAchievementProgress($achievement, amount: 10);
$user->achievementsWithProgress()->get();
$user->achievementsWithSpecificProgress(75)->get();
incrementAchievementProgress() throws Exception if the user does not have the achievement. Grant it first. Progress is capped at 100.
Query Achievements
$user->achievements;
$user->secretAchievements;
$user->allAchievements;
$user->getUserAchievements();
Streaks
Create Activities
use LevelUp\Experience\Models\Activity;
Activity::create(['name' => 'daily-login', 'description' => 'User logs in']);
Record a Streak
$activity = Activity::where('name', 'daily-login')->first();
$user->recordStreak($activity);
- First call: creates streak (count = 1), fires
StreakStarted
- Same day: no-op
- Next consecutive day: increments count, fires
StreakIncreased
- Skipped a day: resets to 1, fires
StreakBroken (archives if enabled)
- Streak frozen: no-op until freeze expires
Query Streaks
$user->getCurrentStreakCount($activity);
$user->hasStreakToday($activity);
$user->streaks;
Reset / Freeze / Unfreeze
$user->resetStreak($activity);
$user->freezeStreak($activity);
$user->freezeStreak($activity, days: 5);
$user->unFreezeStreak($activity);
$user->isStreakFrozen($activity);
Streak History
When a streak breaks, it is archived automatically (if enabled):
use LevelUp\Experience\Models\StreakHistory;
$histories = StreakHistory::where('user_id', $user->id)->get();
Streak Config
'archive_streak_history' => [
'enabled' => env('ARCHIVE_STREAK_HISTORY_ENABLED', true),
],
'freeze_duration' => env('STREAK_FREEZE_DURATION', 1),
Challenges
Challenges are multi-condition goals that users enroll in and complete for rewards. Conditions are evaluated automatically when relevant events fire (points earned, level reached, achievement granted, streak recorded, tier changed).
Create a Challenge
use LevelUp\Experience\Models\Challenge;
Challenge::create([
'name' => 'Getting Started',
'conditions' => [
['type' => 'points_earned', 'amount' => 100],
['type' => 'level_reached', 'level' => 3],
],
'rewards' => [
['type' => 'points', 'amount' => 50],
],
'auto_enroll' => true,
'is_repeatable' => false,
]);
Condition Types
| Type | Required Keys | What it checks |
|---|
points_earned | amount | Points earned since enrollment (baseline delta) |
level_reached | level | User's current level >= value |
achievement_earned | achievement_id | User has the achievement |
streak_count | activity, count | Current streak count for activity >= value |
tier_reached | tier | User is at or above the named tier |
custom | class | Class implementing ChallengeCondition interface |
Reward Types
| Type | Required Keys | What it does |
|---|
points | amount | Awards XP via addPoints() |
achievement | achievement_id | Grants the achievement |
Enrollment
$user->enrollInChallenge($challenge);
$user->unenrollFromChallenge($challenge);
Throws if: challenge not started yet, expired, already enrolled, or completed and not repeatable. Completed repeatable challenges can be re-enrolled.
Auto-enroll challenges (auto_enroll: true) automatically enroll users when a relevant event fires.
Query Progress
$user->getChallengeProgress($challenge);
$user->getChallengeCompletionPercentage($challenge);
$user->activeChallenges;
$user->completedChallenges;
Temporal Constraints
Challenges support optional starts_at and expires_at fields. If both are set, starts_at must be before expires_at. Expired challenges are not evaluated.
Custom Conditions
Implement the ChallengeCondition interface:
use LevelUp\Experience\Contracts\ChallengeCondition;
use Illuminate\Database\Eloquent\Model;
class HasVerifiedEmail implements ChallengeCondition
{
public function check(Model $user, array $condition): bool
{
return $user->hasVerifiedEmail();
}
}
Reference it in the condition: ['type' => 'custom', 'class' => HasVerifiedEmail::class].
Challenge Config
'challenges' => [
'enabled' => env('CHALLENGES_ENABLED', true),
],
Leaderboard
use LevelUp\Experience\Facades\Leaderboard;
Leaderboard::generate();
Leaderboard::generate(paginate: true);
Leaderboard::generate(limit: 10);
Leaderboard::forTier('Gold')->generate();
Returns User models with experience relationship eager-loaded, ordered by XP descending.
Auditing
Enable in config:
'audit' => [
'enabled' => env('AUDIT_POINTS', false),
],
When enabled, every addPoints(), deductPoints(), levelUp(), and tier change creates an experience_audits record.
$user->experienceHistory;
Audit types use the AuditType enum:
use LevelUp\Experience\Enums\AuditType;
AuditType::Add;
AuditType::Remove;
AuditType::Reset;
AuditType::LevelUp;
AuditType::TierUp;
AuditType::TierDown;
Events
| Event | Properties | When |
|---|
PointsIncreased | int $pointsAdded, int $totalPoints, string $type, ?string $reason, Model $user, ?array $multipliers | Points added |
PointsDecreased | int $pointsDecreasedBy, int $totalPoints, ?string $reason, Model $user | Points deducted |
MultiplierApplied | Model $user, Collection $multipliers, int $originalAmount, int $finalAmount, string $strategy | Multipliers modified point calculation |
UserLevelledUp | Model $user, int $level | Level gained (fires per level) |
UserTierUpdated | Model $user, ?Tier $previousTier, ?Tier $newTier, TierDirection $direction | Tier promotion or demotion |
AchievementAwarded | Achievement $achievement, Model $user | Achievement granted at 100% |
AchievementRevoked | Achievement $achievement, Model $user | Achievement revoked |
AchievementProgressionIncreased | Achievement $achievement, Model $user, int $amount | Progress incremented |
StreakStarted | Model $user, Activity $activity, Streak $streak | First streak record |
StreakIncreased | Model $user, Activity $activity, |
Common Patterns
User Profile with Level and Tier
$user = User::with(['experience.status', 'experience.tier'])->find($id);
$data = [
'level' => $user->getLevel(),
'points' => $user->getPoints(),
'next_level_in' => $user->nextLevelAt(),
'level_progress' => $user->nextLevelAt(showAsPercentage: true),
'tier' => $user->getTier()?->name,
'tier_progress' => $user->tierProgress(),
'next_tier_in' => $user->nextTierAt(),
];
Level-Up Reward via Event
use LevelUp\Experience\Events\UserLevelledUp;
Event::listen(UserLevelledUp::class, function (UserLevelledUp $event) {
if ($event->level === 10) {
$achievement = Achievement::where('name', 'Hit Level 10')->first();
if ($achievement) {
$event->user->grantAchievement($achievement);
}
}
});
Seeding Levels and Tiers
class GamificationSeeder extends Seeder
{
public function run(): void
{
Level::add(
['level' => 1, 'next_level_experience' => null],
['level' => 2, 'next_level_experience' => 100],
['level' => 3, 'next_level_experience' => 250],
);
Tier::add(
['name' => 'Bronze', 'experience' => 0],
['name' => 'Silver', 'experience' => 500],
['name' => 'Gold', 'experience' => 2000],
);
Activity::create(['name' => 'daily-login']);
}
}
Config Reference
All model classes in the models config array can be overridden to use custom models. The user.foreign_key defaults to user_id and can be customised for non-standard setups.
return [
'models' => [
'achievement' => LevelUp\Experience\Models\Achievement::class,
'activity' => LevelUp\Experience\Models\Activity::class,
'experience' => LevelUp\Experience\Models\Experience::class,
'experience_audit' => LevelUp\Experience\Models\ExperienceAudit::class,
'level' => LevelUp\Experience\Models\Level::class,
'streak' => LevelUp\Experience\Models\Streak::class,
'streak_history' => LevelUp\Experience\Models\StreakHistory::class,
'achievement_user' => LevelUp\Experience\Models\Pivots\AchievementUser::class,
'tier' => LevelUp\Experience\Models\Tier::class,
'multiplier' => LevelUp\Experience\Models\Multiplier::class,
'multiplier_scope' => LevelUp\Experience\Models\MultiplierScope::class,
'challenge' => LevelUp\Experience\Models\Challenge::class,
'challenge_user' => LevelUp\Experience\Models\Pivots\ChallengeUser::class,
],
'user' => [
=> ,
=> ::,
=> ,
],
=> ,
=> ,
=> [
=> (, ),
=> (, ),
],
=> [
=> (, ),
=> (, ),
=> (, ),
],
=> [
=> (, ),
],
=> [
=> (, ),
],
=> (, ),
=> [
=> (, ),
=> (, ),
=> [],
],
=> [
=> (, ),
],
];
Source: cjmellor/level-up — distributed by TomeVault.