Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Waitlist Laravel Package

offload-project/laravel-waitlist

Laravel Waitlist makes it easy to add waitlist signups to your app. Collect emails, manage entries, and control access with invite or referral codes. Configurable models, notifications, and events help you launch and grow your product.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require offload-project/laravel-waitlist
php artisan vendor:publish --tag="waitlist-config"
php artisan vendor:publish --tag="waitlist-migrations"
php artisan migrate

Before touching anything else, read these in order:

  1. config/waitlist.php — the single most important file. It usually controls the table name, the bound model class, whether the package registers its own routes, the middleware stack applied to those routes, and which notifications fire on signup/confirmation. Most "how do I…" questions are answered here.
  2. routes/waitlist.php (or the Route:: calls in the service provider) — tells you the exact route names (waitlist.store, waitlist.confirm, etc.) and the URIs you are either using or shadowing.
  3. src/Http/Controllers/ — the controller shows what the package expects in the request and what it flashes back to the session, which is what you build your Blade view against.
  4. src/Models/WaitlistEntry.php and the migration — confirms column names (email, name, meta, position, confirmed_at, referrer) so your custom queries don't guess.
  5. src/Events/ — the extension surface. If a feature is missing, an event listener is almost always the intended escape hatch.

First use case: a landing-page form

The package registers a POST route, so the minimum viable integration is a form that posts to it:

{{-- resources/views/welcome.blade.php --}}
@if (session('waitlist.joined'))
    <p>You're #{{ session('waitlist.position') }} in line.</p>
@else
    <form method="POST" action="{{ route('waitlist.store') }}">
        @csrf
        <input type="email" name="email" value="{{ old('email') }}" required>
        <button type="submit">Join the waitlist</button>
    </form>
    @error('email') <p>{{ $message }}</p> @enderror
@endif

Run php artisan route:list --path=waitlist first — if the route name differs in your version, adjust the form action and you're done. Don't build a controller of your own until you've confirmed the package's own doesn't already do what you need.

Implementation Patterns

Headless mode (the most common real-world pattern). For an SPA, mobile client, or Inertia app, disable the package's web routes in the config and call the underlying model/action directly from your own controller. You keep full control of response shape, validation messages, and status codes, and you avoid a Blade view you never render.

// config/waitlist.php
'routes' => ['enabled' => false],

// app/Http/Controllers/Api/WaitlistController.php
public function store(StoreWaitlistRequest $request)
{
    $entry = WaitlistEntry::firstOrCreate(
        ['email' => strtolower($request->validated('email'))],
        ['name' => $request->validated('name'), 'meta' => ['source' => $request->input('utm_source')]],
    );

    WaitlistJoined::dispatch($entry); // if the package's action doesn't already

    return response()->json(['position' => $entry->position], 201);
}

Attach it to the user, not just an email. If your app already has auth, store user_id alongside the entry (add a column via your own migration) and prefer auth()->id() for the uniqueness check once logged in. This makes "upgrade this waitlister to a real account" a one-line update instead of fuzzy email matching.

Referrals and attribution. Referral columns are usually just a code plus a nullable referred_by. Keep the logic in your app: generate the code on WaitlistJoined, capture ?ref= in a middleware that stashes it in the session, and write the referrer when the entry is created. Don't expect the package to compute leaderboards — that's a query you own.

class CaptureReferral
{
    public function handle($request, Closure $next)
    {
        if ($ref = $request->query('ref')) {
            $request->session()->put('waitlist_ref', $ref);
        }
        return $next($request);
    }
}

Gating the product behind the waitlist. A small middleware that checks the currently authenticated user's email against the entries table is the cleanest "private beta" pattern:

public function handle(Request $request, Closure $next)
{
    abort_unless(
        WaitlistEntry::where('email', $request->user()->email)
            ->whereNotNull('confirmed_at')
            ->exists(),
        403
    );

    return $next($request);
}

Admin and export. There is rarely a built-in dashboard at this scale. Either add a Filament/Nova resource against the model, or ship an artisan command that streams a CSV with cursor() so a 50k-row export doesn't blow memory. Filter on confirmed_at — unconfirmed entries are noise in every report.

Testing. Use RefreshDatabase, Mail::fake() and Event::fake() around the signup endpoint so you assert behaviour, not implementation. A duplicate-email test and a "confirmed entry gets access" test cover most regressions when you upgrade.

Gotchas and Tips

  • Duplicate signups will throw, not silently pass. The email column is almost certainly unique. firstOrCreate/updateOrCreate in your own code, or catch QueryException and respond with a friendly "you're already on the list" instead of a 500.
  • Normalise email before it hits the database. Store strtolower(trim($email)). Without this, [email protected] and [email protected] both get in and your unique index doesn't help you.
  • Double opt-in changes your UX. If the package sends a confirmation link, confirmed_at is null on signup and the "you are #42" message is a lie until they click. Decide explicitly whether position is shown pre- or post-confirmation; if the package's default controller reveals it early, override the view or go headless.
  • Route collisions. Publishing routes means the package owns POST /waitlist. If you later add your own route with the same URI, the last registration wins silently. Prefer disabling package routes as soon as you need custom validation, throttling, or JSON responses.
  • CSRF and the web group. API consumers hitting the web route get a 419. Either exclude the route in bootstrap/app.php or disable package routes and expose your own under api.
  • Rate-limit the signup endpoint. A public POST that writes to the DB and sends mail is a spam target. ->middleware('throttle:5,1') in your own route, or a config key if the package exposes one. Pair with a honeypot field — captchas on waitlists hurt conversion.
  • Notifications should queue. If the package sends mail synchronously, signup latency tracks your SMTP provider. Check whether the notification implements ShouldQueue; if not, publish/extend the notification or swap in your own listener and disable the built-in one in config.
  • config:cache in production. Any config value you edit at runtime won't apply until you re-cache. Classic symptom: a table-name or middleware change "doesn't work" locally only after php artisan optimize.
  • Ordering ties. Positions based on id or created_at can collide on bulk imports. If you expose a position, derive it deterministically (orderBy('created_at')->orderBy('id')) or store an explicit integer you maintain.
  • Deletion and GDPR. Provide a way to remove an entry (and any referral links pointing at it, via nullOnDelete). A waitlist is personal data; a waitlist:prune --unconfirmed --older-than=30d command is worth writing on day one.
  • Extension points are the API. Bind your own model class in config/waitlist.php if you need extra columns, and listen to the package's events rather than editing vendor code. Keep customisations in app/, never in vendor/.
  • Small-package reality check. 57 stars and a single maintainer means fewer eyes on edge cases and a real chance of abandoned upgrades. Pin the version in composer.json, skim the diff before bumping, and write a thin adapter class around the package's entry points so a future fork or replacement is a one-file change.
"require": {
    "offload-project/laravel-waitlist": "^1.0"
}
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package