A simple system of referrals with the ability to assign different programs for different users.
This package was created based on the lesson author is Damir Miladinov, with some minor changes, for which I express my gratitude to him.
- Installation
- Documentation Map
- Configuration Reference
- Quickstart
- How It Works
- Sharing and Entry Flows
- Reward Hooks
- Manual Integration Flow
- Bonus
These steps are verified against a fresh Laravel 11, Laravel 12, and Laravel 13 application.
composer require pdazcom/laravel-referralsLaravel registers the service provider automatically through package discovery.
php artisan vendor:publish --tag=referrals-configThis creates config/referrals.php, where you register your referral program classes.
If you prefer the package command, you can run:
php artisan referrals:install --configphp artisan migrateThe package loads its migrations automatically for the default setup, so you do not need to publish them unless you want to customize the migration files before running them.
If you need to customize the migrations, publish them first:
php artisan vendor:publish --tag=referrals-migrationsOr use the package command:
php artisan referrals:install --migrationsIn Laravel 11 and 12, append the middleware to the web stack in bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
\Pdazcom\Referrals\Http\Middleware\StoreReferralCode::class,
]);
})This middleware stores referral links in cookies so they can be attached when the user signs up.
It accepts both legacy UUID links and human-friendly referral codes in the same ?ref= query parameter. For the recommended sharing patterns, see Sharing and Entry Flows.
Add Pdazcom\Referrals\Traits\ReferralsMember to app/Models/User.php:
use Pdazcom\Referrals\Traits\ReferralsMember;
class User extends Authenticatable
{
use HasFactory, Notifiable, ReferralsMember;
}- If you are upgrading from older README instructions, do not edit
app/Http/Kernel.phpin Laravel 11 or 12. Middleware registration moved tobootstrap/app.php. - You only need to publish the package migrations if you want to edit them before running
php artisan migrate.
Starting from v2.0, several referral programs can be applied to the same user. They are stored in cookies as a JSON object, and the request instance exposes them in the
_referralsproperty:[ 'ref_id_1' => 'expires_timestamp', 'ref_id_2' => 'expires_timestamp', ... 'ref_id_n' => 'expires_timestamp' ]
ref_id_nis the referral link ID, andexpires_timestampis the cookie expiration timestamp. Expired links are deleted automatically.
Next: continue with the quickstart to create your first referral program and verify the reward flow.
Use the shortest guide that matches the task you are working on:
| Document | Use it when you need to |
|---|---|
| README.md | Install the package, understand the core flow, or verify a first integration |
| docs/README.md | Browse the docs by topic instead of searching the repo manually |
| docs/sharing-and-entry-flows.md | Decide between share links, human-friendly codes, and manual code entry |
| docs/order-subscription-integration.md | Reward referrers from order or subscription completion events |
| docs/fixed-reward-program.md | Use or adapt the built-in fixed reward program |
| CONTRIBUTING.md | Set up a local checkout, run tests, and open a pull request |
| docs/releases | Review package changes by version |
| docs/research | Read exploratory notes and design directions that are not source-of-truth setup docs |
The package configuration file is config/referrals.php:
return [
'programs' => [
'example' => \Pdazcom\Referrals\Programs\ExampleProgram::class,
],
'fixed_reward_amount' => 10,
'cookie_name' => 'ref',
'code_generator' => \Pdazcom\Referrals\Generators\RandomStringCodeGenerator::class,
'code_length' => 8,
'code_generation_max_attempts' => 10,
'prevent_duplicate_rewards' => false,
'prevent_self_referral' => false,
'hooks' => [
'signup' => false,
'first_purchase' => [
'enabled' => false,
'event' => null,
'programs' => [],
'user_accessor' => 'user',
'reward_accessor' => null,
],
],
];| Key | Default | Required | Behavior |
|---|---|---|---|
programs |
['example' => \Pdazcom\Referrals\Programs\ExampleProgram::class] |
Yes (for reward execution) | Maps referral_programs.name to a reward handler class. RewardUser resolves handler classes via config('referrals.programs.<program_name>'). Missing mappings are skipped with a warning log. |
fixed_reward_amount |
10 |
No | Default flat reward credited by Pdazcom\Referrals\Programs\FixedRewardProgram when you do not override its FIXED_AMOUNT constant in a subclass. |
cookie_name |
'ref' |
No | Controls the query parameter read by StoreReferralCode and the cookie name used to persist active referral link IDs and expiry timestamps. |
code_generator |
\Pdazcom\Referrals\Generators\RandomStringCodeGenerator::class |
No | Container binding used to generate human-friendly referral_code values for new ReferralLink records. |
code_length |
8 |
No | Length passed to the default random string code generator. |
code_generation_max_attempts |
10 |
No | Maximum retries when generating a unique referral_code before throwing a ReferralCodeGenerationException. |
prevent_duplicate_rewards |
false |
No | When enabled, stamps referral_relationships.rewarded_at after the first payout and skips later payouts for the same relationship. |
prevent_self_referral |
false |
No | When enabled, ReferUser skips relationships where the referred user is also the owner of the referral link. |
hooks.signup |
false |
No | When true, automatically dispatches UserReferred on Illuminate\Auth\Events\Registered. Requires StoreReferralCode on the registration route. |
hooks.first_purchase |
(see above) | No | When enabled is true and event is set, automatically dispatches ReferralCase when the configured event fires. See Reward Hooks for full options. |
Use programs to register each referral program name with the class that should run when ReferralCase is dispatched.
- The array key must match the
namevalue stored inreferral_programs. - The class should implement package program behavior (typically by extending
Pdazcom\Referrals\Programs\AbstractProgram). - If no mapping exists for a program name, no reward class is executed for that event.
Example (single program):
'programs' => [
'welcome-bonus' => \App\ReferralPrograms\WelcomeBonusProgram::class,
],Example (multiple programs):
'programs' => [
'welcome-bonus' => \App\ReferralPrograms\WelcomeBonusProgram::class,
'first-purchase' => \App\ReferralPrograms\FirstPurchaseProgram::class,
],cookie_name defines the referral parameter your links use and the cookie key the middleware writes to.
- With the default value (
ref), links look like:/register?ref=ABC123. - If you set
cookie_nametoreferral, links look like:/register?referral=ABC123. - Existing links must use the same query parameter name as your configured
cookie_name.
Every new ReferralLink gets two codes:
code: the legacy UUID used by$link->linkreferral_code: the human-friendly code used by$link->referral_link
The default generator is Pdazcom\Referrals\Generators\RandomStringCodeGenerator, which:
- generates uppercase alphanumeric codes
- avoids visually ambiguous characters such as
0,O,I, and1 - uses
code_lengthto determine the generated code size - retries until the code is unique across both the legacy
codecolumn and the newreferral_codecolumn
If you want custom code generation rules, bind a class that implements Pdazcom\Referrals\Contracts\ReferralCodeGeneratorInterface:
'code_generator' => \App\Referrals\NumericCodeGenerator::class,Use code_generation_max_attempts to cap the retry loop if your code space is small or highly constrained.
Enable this guard when a referred user should only produce one payout for a given referral relationship:
'prevent_duplicate_rewards' => true,Behavior:
RewardUserlocks the matchingreferral_relationshipsrow inside a transaction- the first successful payout stamps
rewarded_at - later
ReferralCaseevents for the same relationship are skipped
Run the package migrations before enabling this guard so the rewarded_at column exists.
Enable this guard if users must not attribute themselves with their own referral links:
'prevent_self_referral' => true,Behavior:
ReferUsercompares the referred user ID with the referral link owner ID- when they match, the relationship is skipped and a log entry is written
- when disabled, self-referrals behave the same as any other referral
This quickstart gives you a verified path from install to the first successful referral relationship and reward dispatch in a fresh Laravel 11 or 12 app.
composer require pdazcom/laravel-referrals
php artisan vendor:publish --tag=referrals-config
php artisan migrateYou do not need to publish the package migrations for the default setup. The package loads them automatically.
Checkpoint: php artisan about should show a Laravel Referrals section.
In Laravel 11 and 12, append the middleware in bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
\Pdazcom\Referrals\Http\Middleware\StoreReferralCode::class,
]);
})Then add the trait to your app/Models/User.php model:
use Pdazcom\Referrals\Traits\ReferralsMember;
class User extends Authenticatable
{
use HasFactory, Notifiable, ReferralsMember;
}Create app/ReferralPrograms/QuickstartProgram.php:
<?php
namespace App\ReferralPrograms;
use Illuminate\Support\Facades\Log;
use Pdazcom\Referrals\Programs\AbstractProgram;
class QuickstartProgram extends AbstractProgram
{
public function reward(mixed $rewardObject): void
{
Log::info('Quickstart reward triggered', [
'program' => $this->program->name,
'recruit_user_id' => $this->recruitUser->id,
'referral_user_id' => $this->referralUser->id,
'reward' => $rewardObject,
]);
}
}Register it in config/referrals.php:
'programs' => [
'quickstart' => \App\ReferralPrograms\QuickstartProgram::class,
],php artisan tinker --execute='use App\Models\User; use Pdazcom\Referrals\Models\ReferralLink; use Pdazcom\Referrals\Models\ReferralProgram; $referrer = User::firstOrCreate(["email" => "referrer@example.com"], ["name" => "Referrer", "password" => "secret123"]); $program = ReferralProgram::firstOrCreate(["name" => "quickstart"], ["title" => "Quickstart Program", "description" => "Quickstart verification", "uri" => "/register", "lifetime_minutes" => 60]); $link = ReferralLink::firstOrCreate(["user_id" => $referrer->id, "referral_program_id" => $program->id]); echo json_encode(["referrer_id" => $referrer->id, "program_id" => $program->id, "link_id" => $link->id, "code" => $link->code, "url" => $link->link], JSON_PRETTY_PRINT);'Checkpoint: the output includes a link_id, code, and url.
php artisan tinker --execute='use App\Models\User; use Pdazcom\Referrals\Events\UserReferred; use Pdazcom\Referrals\Models\ReferralLink; use Pdazcom\Referrals\Models\ReferralRelationship; $link = ReferralLink::firstOrFail(); $email = "referred+" . now()->timestamp . "@example.com"; $user = User::create(["name" => "Referred User", "email" => $email, "password" => "secret123"]); UserReferred::dispatch([$link->id => now()->addHour()->timestamp], $user); $relationship = ReferralRelationship::where("user_id", $user->id)->first(); echo json_encode(["referred_user_id" => $user->id, "relationship_exists" => (bool) $relationship, "relationship_link_id" => $relationship?->referral_link_id], JSON_PRETTY_PRINT);'Checkpoint: relationship_exists is true.
php artisan tinker --execute='use App\Models\User; use Pdazcom\Referrals\Events\ReferralCase; $user = User::latest("id")->firstOrFail(); ReferralCase::dispatch("quickstart", $user, ["order_total" => 1500]); echo json_encode(["rewarded_user_id" => $user->id], JSON_PRETTY_PRINT);'
tail -n 5 storage/logs/laravel.logCheckpoint: the log contains Quickstart reward triggered.
At this point the package is installed, the referral relationship is stored, and the reward handler is running. To wire this into your real registration flow, dispatch UserReferred::dispatch($request->input(StoreReferralCode::REFERRALS), $user) after signup as shown below.
If you want to support code sharing in chat, SMS, or native mobile flows, continue with Sharing and Entry Flows.
The package is event-driven. This is the shortest path through the main objects:
ReferralLink shared or entered
|
v
StoreReferralCode middleware captures ?ref=... and stores active referral IDs in a cookie
|
v
UserReferred event is dispatched after signup or via registerWithCode()
|
v
ReferUser creates ReferralRelationship rows
|
v
ReferralCase event is dispatched when a qualifying conversion happens
|
v
RewardUser resolves the program class and calls reward()
Core models:
ReferralProgram: defines the program name, target URI, and attribution lifetimeReferralLink: belongs to a user and program, stores both the legacy UUID code and human-friendlyreferral_codeReferralRelationship: records that a user was referred by a specific referral link and whether they have already been rewarded
Integration choices:
- use hooks when your app already fires
Registeredand a purchase or subscription event - dispatch
UserReferredandReferralCasemanually when you need explicit control - use
registerWithCode()when referral attribution happens through a typed code instead of a clicked link
Use this section to choose the referral flow that matches your product surface. The package now supports both shareable links and code-only attribution without breaking existing link-based integrations.
For a deeper guide with examples and verification steps, see docs/sharing-and-entry-flows.md.
| Flow | Best for | What you share | What the user does |
|---|---|---|---|
referral_link |
Chat, email, SMS, landing pages | Human-friendly link such as /register?ref=INVITE2024 |
Opens the link and signs up normally |
referral_code |
Support flows, native mobile apps, offline campaigns | Short code such as INVITE2024 |
Types or pastes the code into your app |
link |
Backward-compatible integrations | UUID link such as /register?ref=550e8400-e29b-41d4-a716-446655440000 |
Opens the legacy link |
Use referral_link when you want a readable URL for public sharing:
$link = ReferralLink::create([
'user_id' => $user->id,
'referral_program_id' => $program->id,
]);
$shareUrl = $link->referral_link;
$shareCode = $link->referral_code;This is the recommended default for web, email, SMS, and messaging apps because the same code can also be shown separately for manual entry.
Use registerWithCode() when the referred user enters a code directly instead of visiting a link. The method accepts either the human-friendly referral_code or the legacy UUID code.
use Illuminate\Http\Request;
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string'],
'email' => ['required', 'email'],
'password' => ['required', 'string'],
'referral_code' => ['nullable', 'string'],
]);
$user = User::create($data);
if (!empty($data['referral_code'])) {
$user->registerWithCode($data['referral_code']);
}
return redirect('/dashboard');
}registerWithCode() returns true when the code resolves to a referral link and false when the code is unknown, so you can decide whether to show validation feedback or continue without attribution.
The original link attribute still returns a URL with the UUID-based code:
$legacyUrl = $link->link;This keeps older integrations working. New user-facing sharing surfaces should prefer referral_link and referral_code.
- Create a referral link and note both
$link->referral_linkand$link->referral_code. - Visit the share URL and confirm the middleware redirects to a clean URL and stores the referral cookie.
- Complete signup and confirm a
referral_relationshipsrow exists for the new user. - Repeat the same attribution using
$user->registerWithCode($link->referral_code)and confirm you get the same relationship result.
Reward hooks let you trigger referral events automatically in response to standard application events, without adding manual event dispatch to every controller or service. All hooks are opt-in and disabled by default.
The signup hook listens to Illuminate\Auth\Events\Registered and automatically dispatches UserReferred for any referral link stored in the current request by the StoreReferralCode middleware.
Requirements:
StoreReferralCodemust be active on your registration route.- Your registration flow must fire
Illuminate\Auth\Events\Registered(Laravel's built-inRegisteredControllerand Fortify/Breeze do this automatically).
Enable in config/referrals.php:
'hooks' => [
'signup' => true,
],When enabled, you no longer need to manually dispatch UserReferred in your registration controller. The hook handles it automatically as long as the referral cookie is present on the request.
The first-purchase hook listens to a configurable application event and dispatches ReferralCase for the configured programs. This is useful when you want to reward the referrer when a referred user makes their first purchase.
Enable and configure in config/referrals.php:
'hooks' => [
'first_purchase' => [
'enabled' => true,
'event' => \App\Events\OrderCreated::class,
'programs' => ['welcome-bonus', 'first-purchase'],
'user_accessor' => 'user',
'reward_accessor' => 'order',
],
],Options:
| Key | Default | Description |
|---|---|---|
enabled |
false |
Set to true to activate the hook. |
event |
null |
Fully-qualified class name of the event to listen for. Must be set when enabled is true. |
programs |
[] |
Array of referral program names to reward. Must match name values in referral_programs table. |
user_accessor |
'user' |
Property or zero-argument method name on the event that returns the referred Eloquent user model. |
reward_accessor |
null |
Property or zero-argument method name on the event to use as the $rewardObject passed to ReferralCase. When null, the event object itself is passed. |
Example event:
namespace App\Events;
class OrderCreated
{
public function __construct(
public \App\Models\User $user,
public \App\Models\Order $order,
) {}
}With the config above, ReferralCase::dispatch(['welcome-bonus', 'first-purchase'], $event->user, $event->order) is dispatched automatically whenever OrderCreated fires.
Note: The hook dispatches
ReferralCaseevery time the configured event fires. If you want to reward only on the first purchase, add a guard inside your program'sreward()method (for example, check whether a reward has already been recorded for this user).
Enabling hooks does not change any existing behavior. Existing manual dispatches of UserReferred and ReferralCase continue to work. You can keep manual dispatches alongside hooks without double-rewarding as long as you are not dispatching the same event twice for the same user action.
Use this section when you do not want to enable hooks and prefer to dispatch package events yourself.
use Illuminate\Http\Request;
use Pdazcom\Referrals\Events\UserReferred;
use Pdazcom\Referrals\Http\Middleware\StoreReferralCode;
public function registered(Request $request, $user): void
{
UserReferred::dispatch(
$request->input(StoreReferralCode::REFERRALS, []),
$user,
);
}This consumes the referral IDs that StoreReferralCode placed on the request and creates ReferralRelationship records through the ReferUser listener.
If you collect a typed referral code instead of relying on the middleware cookie, call:
$user->registerWithCode($request->string('referral_code')->toString());php artisan tinkerPdazcom\Referrals\Models\ReferralProgram::create([
'name' => 'example',
'title' => 'Example Program',
'description' => 'Example percentage-based reward program.',
'uri' => '/register',
'lifetime_minutes' => 60,
]);Then map the program name to your reward handler in config/referrals.php:
'programs' => [
'example' => \App\ReferralPrograms\ExampleProgram::class,
],<?php
namespace App\ReferralPrograms;
use Pdazcom\Referrals\Programs\AbstractProgram;
class ExampleProgram extends AbstractProgram
{
private const ROYALTY_PERCENT = 30;
public function reward(mixed $rewardObject): void
{
$this->recruitUser->balance += $rewardObject * (self::ROYALTY_PERCENT / 100);
$this->recruitUser->save();
}
}Pdazcom\Referrals\Models\ReferralLink::create([
'user_id' => 1,
'referral_program_id' => 1,
]);The created model exposes:
$link->referral_linkfor human-friendly share URLs$link->referral_codefor manual entry surfaces$link->linkfor legacy UUID-based URLs
use Pdazcom\Referrals\Events\ReferralCase;
ReferralCase::dispatch('example', $referralUser, $rewardObject);RewardUser will:
- resolve the matching
ReferralProgram - find the
ReferralLinkthat attributed the referred user - load the configured reward class from
config('referrals.programs.<name>') - call
reward($rewardObject)on that class
If you want to list all the users for a given Referral Link, simply use
$referralLink->referredUsers()See CONTRIBUTING.md for local setup, how to run tests, and the pull request workflow.
If you discover any security related issues, please email kostya.dn@gmail.com instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.