mateusjunges/laravel-invite-codes
Create, manage, and track invite codes in Laravel. Generate unique codes, set expiration dates and usage limits, associate codes with users or models, and record redemptions—ideal for beta signups, referrals, or gated registration.
mateusjunges/laravel-invite-codes is a small, focused package that does one job: generate, hand out, expire, and redeem invite codes for your application's users (or any Eloquent model). It ships with migrations, a config file, a trait, a facade, a route middleware, and events — no UI, no queue jobs, no opinions about how you gate signup.
Install and scaffold (3 commands):
composer require mateusjunges/laravel-invite-codes
php artisan vendor:publish --tag=invite-codes-migrations
php artisan vendor:publish --tag=invite-codes-config
php artisan migrate
First use case — invite-only registration. The 15-minute path:
User):use Mateusjunges\InviteCodes\Traits\HasInviteCodes;
class User extends Authenticatable
{
use HasInviteCodes;
}
$code = $user->createInviteCode(); // single
$codes = $user->createInviteCodes(5); // batch, returns a collection
if (! $user->redeemInviteCode($request->input('invite_code'))) {
return back()->withErrors(['invite_code' => 'Invalid or expired code.']);
}
Where to look first: read config/invite-codes.php end-to-end before writing any code. It controls code length, character set, prefix/suffix, default max uses, default expiry, and the middleware alias. Then skim the published migrations — the column names (code, max_uses, uses, expires_at, plus the owner/invitee foreign keys) tell you more about the package's mental model than the README does.
On v2.4.0: this release adds Laravel 13 support and nothing else — no API changes, no deprecations, no config keys added or removed. If you are already on ^2.3, upgrading is a no-op for your code. If you are starting a Laravel 13 project, this is the first version that will install cleanly; pin "mateusjunges/laravel-invite-codes": "^2.4" so a future composer update on a fresh checkout can't resolve down to a pre-L13 release on a clean install.
Two entry points, same underlying model. You can drive everything through the HasInviteCodes trait (fluent, scoped to a user) or through the InviteCodes facade (global, useful in jobs, seeders, and admin panels):
use Mateusjunges\InviteCodes\Facades\InviteCodes;
// Trait style: code is owned by $user
$user->createInviteCode();
// Facade style: chainable builder
InviteCodes::create()->for($user)->withMaxUses(3)->save();
InviteCodes::create()->expiresIn('7 days')->save();
InviteCodes::create()->expiresAt(now()->addMonth())->save();
// Lookups and cleanup
InviteCodes::find($code);
InviteCodes::getAll();
InviteCodes::delete($code);
Prefer the builder for anything with policy attached (withMaxUses, expiresAt) and the trait for the "give this user a code" button in an admin panel. Mixing the two is fine — they produce the same rows.
Multi-use team invites. max_uses is the feature that separates this package from a random string column:
InviteCodes::create()
->for($teamOwner)
->withMaxUses(10)
->expiresIn('30 days')
->save();
Every successful redeemInviteCode() increments uses and stamps the invitee. Render max_uses - uses in your UI as "seats remaining" rather than exposing the raw counters.
Middleware as a cheap gate. The package registers a route middleware alias (configurable in config/invite-codes.php) that pulls the code out of the request and rejects invalid ones before your controller runs:
Route::post('/register', RegisterController::class)
->middleware(['invite-code']);
This is the fastest way to bolt invite-gating onto an existing registration flow without touching the controller. Use it when the only requirement is "no valid code, no entry." Drop it and validate manually when you need custom error messages, per-plan limits, or to record which code was used for attribution.
Wrap creation in a transaction when it's part of onboarding. Inviting a user typically means creating the invite code and sending an invite record/mail. Do the DB work in a transaction and dispatch the notification after commit, otherwise a rolled-back invite can leave a live code circulating:
DB::transaction(function () use ($user) {
$code = $user->createInviteCode();
Invite::create(['code' => $code->code, 'email' => $email]);
});
Hook the events for side effects. Code creation and redemption both fire events. Listen to them for audit logging, analytics, or "your invite was used" notifications to the inviter — cleaner than decorating controllers:
Event::listen(InviteCodeRedeemed::class, function ($event) {
// notify inviter, increment a funnel metric, etc.
});
Seeders and local dev. InviteCodes::create()->for(User::factory()->create())->save() in a seeder gives QA a known code. Better: generate a fixed, readable code (short code_length, digits only) in local/testing env via a config override so test fixtures aren't hunting random strings.
Testing. Use RefreshDatabase plus the facade to create codes inline. Assert both the happy path and the three failure modes: wrong code, expired (expires_at in the past), and exhausted (uses >= max_uses). Those three are where this package's bugs will live in your app.
code column is unique; with the default character set, an 8-character code has plenty of entropy, but if you shorten code_length to 4 for "nicer" invite codes, expect intermittent uniqueness failures under load. Bump length, not retry loops, unless you have measured a problem.redeemInviteCode() failing is a normal return, not an exception. It returns falsy for invalid/expired/exhausted codes. Always branch on the result — a bare $user->redeemInviteCode($code); that ignores the return value is the single most common integration bug with this package.expires_at = null means never expires. If you want default expiry, set it in config; if you want no codes to be permanent, enforce that at your creation call sites or in a model observer. Nothing stops a caller from omitting expiresIn().users table for the owner/invitee foreign keys. Non-standard user tables, UUID/ULID keys, or a separate teams owner model require editing the published migration — once it has run in production, this becomes a manual migration.code_length, prefix/suffix, or the middleware alias in a cached environment does nothing until php artisan config:cache runs again. This burns people in CI where config:cache is part of the build step.code_characters or code_length does not retroactively affect existing codes. Old codes keep validating; only new ones use the new format. Fine in practice, confusing during debugging.invite_code by default). If your form field is named code, you'll get a rejection that looks like a validation logic bug. Confirm the key before blaming the package.onDelete behaviour in the published migration and add a cascade or a pruning job if orphaned codes are a concern.^2.4 is your floor — older tags will fail dependency resolution.TEAM-XXXX-XXXX), the cleanest route is a wrapper service that uses the facade with an explicit prefix/suffix, not forking the package. For custom validation semantics (domain restrictions, one-code-per-email), keep that logic in your own FormRequest and use the package purely as storage.How can I help you explore Laravel packages today?