Library / SDK
cjmellor/level-up avatar
cjmellor/level-up

Level-Up: XP, levels and leaderboards inside a Laravel app

Level-Up is a Laravel package introducing gamification into your applications. Users earn experience points (XP) and levels through interactions, while also unlocking achievements. It promotes engagement, competition, and fun through its dynamic leaderboard feature. Customisable to fit your specific needs

675 stars54 forksPHPMIT

At a glance

What is it?
Level-Up is a Laravel package that records user activity as experience points, promotes users through levels, and adds achievements, streaks, tiers, multipliers, challenges and leaderboard snapshots on top of your existing models. It is configurable down to identifier types and table names, which is where the real decisions sit.
Who is it for?
Level-Up suits Laravel teams that want XP, levels and a leaderboard in a product rather than in a demo, and it suits teams less well if they need a front end, since the repository ships no UI.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Level-Up records activity as XP rather than shipping a game

Level-Up is a Composer package for Laravel that gives users experience points and levels based on actions they perform on your site, plus achievements they can earn along the way. The README is careful to frame it as a way to track user progress and add gamification elements to an application, not as a game engine. The domain model is the point: there is no separate service to host and no client to embed, the package adds data structures to your Laravel app and a set of actions that write to them.

The feature list is broad. Users earn XP and progress through levels, they collect achievements, streaks track repeated activity, tiers and multipliers modify what activity is worth, challenges are completed by users, divisions and cohorts segment the population, and leaderboard snapshots capture standings. The package description highlights the dynamic leaderboard and calls the whole thing customisable to fit specific needs, and the configuration file backs that up.

The repository layout shows how the code is organised. There is a src directory for the package code, a config directory for the publishable configuration, a database directory for migrations, a resources directory for whatever the package ships to the application, and a tests directory. Development tooling is visible too, with phpstan.neon, phpunit.xml.dist, pint.json and rector.php, so static analysis, tests and automated refactoring are all wired into the project. The default branch is 3.x, matching the current major version.

Twenty models cover the whole progression system

The published config file lists every model the package owns, and the list is longer than a gamification package usually needs. Core progression is handled by the Experience, Level and ExperienceAudit models, with Achievement for the badges users earn. Streak and StreakHistory track repeat behaviour over time. Tier and Multiplier sit alongside the pivot models MultiplierUser and MultiplierTier, which is how a per-user or per-tier multiplier is applied. Challenge, ChallengeUser and ChallengeCompletion cover time or task based goals, LeaderboardSnapshot records standings, and Division and Cohort with its CohortUser pivot segment the user base for competition.

Each of those models is swappable. The models array maps a short key such as achievement or leaderboard_snapshot to a class, and the defaults point at the LevelUp\Experience\Models namespace:

php
    '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,

Because the mapping is configuration, an application can point a key at its own class instead of the package default. That is the mechanism behind the package's flexibility claim: you replace the model class, and the rest of the package follows the config. It is also the reason the config file is so long. The price of that flexibility is that you have to read the config carefully before your first migration, because several of these settings are hard to change afterwards.

Installing the package and publishing its migrations and config

Installation is a standard Composer require, and the README then splits the setup into migrations and configuration:

bash
composer require cjmellor/level-up

The migrations are published by tag and then run:

bash
php artisan vendor:publish --tag="level-up-migrations"
php artisan migrate

The configuration file is published separately:

bash
php artisan vendor:publish --tag="level-up-config"

The split matters because of what the configuration controls. The identifier type for the package tables and the column type of the user foreign key are both set in the config, and both are baked into the migrations when they run. The published file states this explicitly for the user key, telling you to set foreign_key_type to uuid or ulid if your host User model uses HasUuids or HasUlids, and to leave it as bigint for standard auto-increment user IDs, with the note that this only affects fresh migrations and that existing installs keep whichever column type they originally migrated with. The same warning appears for the package's own identifiers under entities.id_type, where uuid or ulid is described as a way to make package IDs opaque for safe exposure on a public API surface.

The README also points to a Customizing Identifiers section for guidance on switching an existing install. The practical reading is that you should decide these two values before the first migrate, not after, because the migration itself is what fixes the column types in your database.

Table names and prefixes are configuration, not migrations

Table naming is handled entirely in the config file, with three interacting keys. The table_prefix value is prepended to every default package table name, and it reads from an environment variable:

php
    'table_prefix' => env('LEVEL_UP_TABLE_PREFIX', ''),

The tables array then holds the name for each model table. The rule stated in the file is that a value equal to the default applies the prefix, while setting it to any other string overrides that table's name exactly with no prefix applied. Per-table overrides are therefore taken verbatim and are not prefixed:

php
    'tables' => [
        'experiences' => 'experiences',
        'experience_audits' => 'experience_audits',
        'levels' => 'levels',
        'achievements' => 'achievements',
        'achievement_user' => 'achievement_user',
        'streaks' => 'streaks',
        'streak_histories' => 'streak_histories',
        'streak_activities' => 'streak_activities',

This is a sensible design for a package that shares a database with an application, since a prefix lets you drop the package tables into a schema-like namespace without editing migrations. It also means the table names in your database are not guaranteed to match the keys in the config, so a deployment that changes LEVEL_UP_TABLE_PREFIX after the first migration leaves the package reading tables that no longer exist. The config file gives no rollback procedure for that case, and the README does not document one either.

Leaderboards, divisions and cohorts, with the config cut short

The competitive side of the package is built around the LeaderboardSnapshot model, with Division and Cohort providing the segmentation that makes a leaderboard mean something. A snapshot model implies that standings are captured at a point in time rather than computed on every page load, which is the design you want for a leaderboard that has to stay fast as the user table grows, and it also gives you the option of showing historic positions. The naming of the table, leaderboard_snapshots, matches the model key in the models array.

The published config file ends mid-section in the version of the file quoted in the README, right where the leaderboard block begins and a comment explains what the metrics map does. The map from metrics to leaderboard entries is therefore not visible in the documentation available here, and the README excerpt does not show the remaining configuration keys. What is clear is the shape: a leaderboard section with a metrics map, sitting alongside the models, user, entities, table_prefix and tables sections. Coherent defaults for leaderboard metrics are the kind of thing you will have to read in the published file rather than in the README, and it is worth doing before you design a leaderboard screen.

The resources directory in the repository layout is the place to look for anything the package ships to the application, such as views or translations. The README excerpt does not list its contents, so treat it as an unknown rather than assuming a bundled UI.

Where Level-Up is the wrong tool

The clearest case is a product that needs a front end. Level-Up is a backend package: it owns models, migrations and configuration, and the README shows no Blade views, no Livewire components and no JavaScript. The gamification part of your product, the level badge on the profile page, the XP counter, the leaderboard screen, is yours to build, and that work is usually larger than the work of wiring up the package. The banner in the README advertises a dynamic leaderboard, but the deliverable is the data, not the screen.

The second case is an application that already has a progression model of its own. If your users already have a points balance, a tier and a history table, migrating means mapping your schema onto the package's model classes and the identifier decisions described above, and then maintaining that mapping forever. For a new product, adopting the schema is free. For an existing one, it is a migration with a hard-to-reverse column type choice.

The third case is a team that wants full control of the schema. The configuration is extensive, but it is not unlimited, and the config file itself flags the limits: the id_type and foreign_key_type settings only affect fresh migrations, and existing installs keep the type they were migrated with. If your application already uses ULIDs everywhere and you need the package tables to match, you are reading the Customizing Identifiers section and possibly the UPGRADE.md file before you install, not after.

Version 3, the upgrade notes and a permissive licence

The release history is short and recent. Version 2.1.0 shipped on 2026-05-24, a 3.0.0-beta1 followed on 2026-05-25, and v3.0.0 landed on 2026-07-10. That is a major version in under two months after the last 2.x release, so the 3.x branch is where new work happens and the default branch reflects that. The last push was on 2026-10-14, so the branch is still receiving changes rather than sitting frozen at the release tag.

The repository includes an UPGRADE.md file next to the CHANGELOG.md, and both are the places to look when moving an existing application between major versions. The README itself does not describe the 2.x to 3.x migration steps, which is exactly the kind of information a project of this kind keeps in the upgrade notes rather than in the readme. Laravel compatibility is advertised in the badge as version 12 or 13, and the package targets a specific PHP range, so the host application has to satisfy both before install.

The licence is MIT, which is permissive and easy to live with inside a commercial Laravel application. It covers the package code; it says nothing about the achievements, badges and copy you author for your own product, and it does not oblige you to open-source the game design on top of it. For a package whose whole purpose is to sit inside someone else's commercial application, that is the right licence, and it is a simpler question than the one the identifier settings pose.

Editorial conclusion

Level-Up suits Laravel teams that want XP, levels and a leaderboard in a product rather than in a demo, and it suits teams less well if they need a front end, since the repository ships no UI. Before installing, decide the id_type and foreign_key_type in the config, because the README states those settings only affect fresh migrations and that existing installs keep the column type they were migrated with, and read UPGRADE.md before moving an application from the 2.x line to v3.0.0, released on 2026-07-10.

Frequently asked questions

How do I install Level-Up in a Laravel app?

Run composer require cjmellor/level-up, then publish and run the migrations with php artisan vendor:publish --tag="level-up-migrations" followed by php artisan migrate, and publish the configuration with php artisan vendor:publish --tag="level-up-config".

What does the Level-Up config file let me change?

It lets you swap the model classes, set the user foreign key and its column type, choose the package identifier type, prefix the table names through LEVEL_UP_TABLE_PREFIX, override individual table names, and configure the leaderboard section.

Can I use UUID or ULID identifiers with Level-Up?

Yes. Set foreign_key_type to uuid or ulid in the user config if your User model uses HasUuids or HasUlids, and set entities.id_type to uuid or ulid to make the package identifiers opaque. Both only affect fresh migrations, so existing installs keep their original column type.

Which Laravel versions does Level-Up support?

The README badge shows compatibility with Laravel version 12 or 13, and the package also targets a specific PHP version, so the host application has to satisfy both before the package can be installed.

What is the difference between Level-Up 2.x and 3.x?

The README does not describe the migration steps between the two major versions. Version 2.1.0 was released on 2026-05-24 and v3.0.0 on 2026-07-10, and the repository keeps the details in UPGRADE.md and CHANGELOG.md.

Official sources

  1. cjmellor/level-up on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cjmellor-level-up.svg)](https://hysenlabs.com/projects/cjmellor-level-up)

Was this useful?