CommunityProgramación y desarrollogithub.com

Taldres/laravel-waitlist

Integrate taldres/laravel-waitlist in a Laravel application: define purposes, lists and fields, build the signup form, send the confirmation mail from events, add a preference page, and keep consent, retention and encryption intact.

¿Qué es laravel-waitlist?

laravel-waitlist is a Claude Code agent skill that integrate taldres/laravel-waitlist in a Laravel application: define purposes, lists and fields, build the signup form, send the confirmation mail from events, add a preference page, and keep consent, retention and encryption intact.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Taldres/laravel-waitlist/tree/HEAD/resources/boost/skills/laravel-waitlist-development

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Laravel Waitlist

Use this skill when a Laravel application collects email addresses for a waitlist, early access or launch notifications with taldres/laravel-waitlist.

Primary Goal

  • apply the package's public API in the smallest correct way, preserving its technical privacy controls: consent records, double opt-in, encryption, retention

Workflow

1. Install

Requires PHP 8.3+ and Laravel 13.

composer require taldres/laravel-waitlist
php artisan waitlist:install   # provider, config and migrations; registers the provider
php artisan migrate

waitlist:install publishes app/Providers/WaitlistServiceProvider.php (waitlist-provider), config/waitlist.php (waitlist-config) and the migrations (waitlist-migrations). Laravel's scheduler must run: the package schedules waitlist:prune itself.

2. Define purposes, lists, fields and pages

Describe the waitlist in boot() of the published provider; config/waitlist.php keeps only the technical settings.

// app/Providers/WaitlistServiceProvider.php
use Taldres\Waitlist\Definitions\ProjectDefinition;
use Taldres\Waitlist\Facades\Waitlist;

Waitlist::define(function (ProjectDefinition $project): void {
    $project->purpose('waitlist', ['2026-10' => 'Email me when early access opens.']);
    $project->purpose('newsletter', ['2026-10' => 'Also send me the monthly newsletter.']);

    $project->list('default', purpose: 'waitlist')->optional('newsletter');
});
  • Every list has exactly one required primary purpose; anything else is optional.
  • Only defined lists accept signups; $project->list('*', purpose: ...) covers any other list name. ->doubleOptIn(false) turns double opt-in off for one list.
  • Read the environment through config(), never env(): with a cached config, env() returns null outside the config files. The callback runs when the project is first needed, and defining a project again replaces it.
  • Fields beyond the address, per project and list: $project->fields(fn () => [...]) for every list, ->fields(...) on a list on top (field => validation rules; a closure when the rules are objects). The HTTP signup accepts only these under metadata and refuses the rest with a 422; without fields it refuses all metadata. add() trusts its caller: in your own controllers validate with Waitlist::for($list)->fields().
  • Several products in one app: one Waitlist::define('<key>', ...) per product with its own purposes, lists, fields and pages, and use Waitlist::project('<key>')->for($list). Without project() the facade always means the default project, never all of them; an unknown project throws UnknownProjectException. For projects kept in a database, bind your own ProjectCatalog (waitlist.catalog): it supplies lists, fields, wording versions and frontend URL patterns per project. Over HTTP, a ProjectResolver (waitlist.project_resolver) picks the project of a signup, e.g. from a publishable key; never put that check into routes.middleware, which also guards the token links in mails. Checks for the forms only, such as CSRF or an origin check, go into routes.group_middleware.signup.
  • Frontend pages per project in $project->urls(...): confirm, unsubscribe, manage (mail links, {token} replaced) and confirmed, expired, invalid, unsubscribed, erased (where a browser lands after posting).
  • Rate limits per group in routes.limiters: signup → waitlist (per IP), links → waitlist-links (per token). Tune routes.rate_limits, point a group at your own RateLimiter::for() name (a name nothing defines is refused), or set it to null. An own limiter decides the key (->by()), the limits, exemptions (Limit::none()), per-route rules ($request->routeIs()) and the response; rate_limits does not apply to it. Never key links by IP alone: one-click unsubscribes share mail providers' IPs. A caller acting for a project (HasWaitlistProject) is a server: it is capped as a whole (rate_limits.caller_signup_per_minute) and limits its visitors itself, unless it forwards their address in the header named by authentication.client_ip_header, which then counts per visitor too.
  • To change wording, add a new version key. Never edit an existing version's text.
  • Several languages: a version holds ['en' => ..., 'de' => ...]. Render Waitlist::purposes($list, locale: $locale) (or GET /waitlist/purposes?locale=) and post back {version, locale} as served; such a version needs the locale.
  • A form rendering its own copy of the text (CMS) adds hash = sha256 of the shown text; a mismatch is a 422. WAITLIST_REQUIRE_WORDING_HASH=true requires it for every consent newly recorded, not for keeping or withdrawing one.
  • Wording written in the frontend or CMS: set waitlist.catalog to StoredWordingCatalog and register it on deploy with php artisan waitlist:wording wording.json (purpose => version => text; first --from-definitions to carry over what purpose() holds), or Waitlist::registerWording($purpose, $version, $wording). Registered text is immutable: a change needs a new version.
  • Wording sent by the project's own servers: $project->wordingFromCallers(); a signup posts {version, locale?, text} and a new version is registered with the server (registered_by, event WordingRegistered). Other text for a known version is a 422; guests sending text get a 403 (gate action RegisterWording).

3. Signup

Render the wording from the server and post back the versions shown:

use Taldres\Waitlist\Facades\Waitlist;

$wording = Waitlist::purposes('default'); // list<PurposeWording>: purpose, version, text, required, locale; hash()

$result = Waitlist::for('default')->add($email, ['waitlist' => '2026-10', 'newsletter' => '2026-10']);

Over HTTP (with WAITLIST_ROUTES_ENABLED=true): GET /waitlist/purposes?list=default, then POST /waitlist with email, list, and purposes as {purpose: version}. Fields beyond the address go under metadata, and only those the project or list defines are accepted. While the routes are off every route answers 404, before any rate limit or session; an empty WAITLIST_ROUTES_ENABLED= is a config error, false turns them off.

4. Mails come from your listeners

The package never sends mail. Listen to EntrySubscribed for the confirmation mail:

use Taldres\Waitlist\Events\EntrySubscribed;

public function handle(EntrySubscribed $event): void
{
    if (! $event->requiresConfirmation) {
        return;
    }

    Mail::to($event->entry->email)->send(new ConfirmWaitlist(
        confirmUrl: $event->confirmUrl,
        purposes: $event->subscription->consents,
        unsubscribeUrl: $event->unsubscribeUrl,
    ));

    // Record the mail reference alongside the consent wording.
    Waitlist::confirmationMailed($event->subscription, 'confirm-mail@2026-10');
}
  • $event->confirmUrl comes from the confirm page of the project's urls() or, with WAITLIST_ROUTES_ENABLED=true, the package route. With neither it is null: set one before the first signup, or the mail goes out without a link. A link that cannot be built (the package routes are on but not registered, as after a stale route cache) throws InvalidConfigurationException and undoes the signup or confirmation: nothing is committed and no event fires.
  • Every later mail carries a link that withdraws exactly its purpose: Waitlist::unsubscribeUrl($entry, 'newsletter') and Waitlist::listUnsubscribeHeaders($entry, 'newsletter').
  • Queued listeners implement ShouldBeEncrypted: payloads hold tokens and, for EntryForgotten, the address in plain text.

5. Before sending, check consent

Send to recipients(): only addresses with the purpose in force, once each, with the right links (unsubscribeUrl(), listUnsubscribeHeaders()) and the consent's locale.

Waitlist::recipients('newsletter')->each(fn (Recipient $recipient) => ...);  // project-wide; links withdraw the purpose (a primary purpose: that list only)
Waitlist::for('beta')->recipients()->each(...);                              // one list; links leave it
$entry->hasConsentFor('newsletter');

6. Preference page and rights

  • Mails carry only the unsubscribe token, which can only remove. The preference page needs a short-lived manage token, mailed on request: listen to ManageLinkRequested and send $event->manageUrl to $event->entry->email. Requests: POST /waitlist/manage-link with token (the unsubscribe token) or email, Waitlist::requestManageLink($token), Waitlist::for($list)->requestManageLink($email).
  • With routes on, the page uses GET /waitlist/manage/{token}, PUT .../purposes, POST .../data, POST .../unsubscribe, and POST .../erase with {"confirm": true}; 410 once the link expired.
  • On request: Waitlist::allProjects()->personalData($email), Waitlist::allProjects()->forget($email) (every project; one controller), Waitlist::for($list)->withdraw($email, $purpose), and Waitlist::for($list)->forgetAll() once the list's purpose is fulfilled.

Rules, References, and Templates

  • Events: EntrySubscribed, EntryConfirmed, ConsentGranted, ConsentWithdrawn, EntryUnsubscribed, SubscriptionExpired, EntryForgotten, ManageLinkRequested; all dispatch after commit.
  • Config is read where it is used and refused when it does not read, with an InvalidConfigurationException that names the key. .env takes true/false, on/off, yes/no, 1/0 and whole numbers (05 is 5); anything else is refused, an empty KEY= line included: delete the line to keep the default. null says never or off (double_opt_in.token_ttl, retention periods, cooldowns, caps, a limiter, retention.schedule); a manage link always expires. A confirm or manage link may not end after 2038-01-19, and a retention period or cooldown may not reach back before 1970, so a 36500-day "forever" is refused: write null. A cron expression that can never run and a route prefix with {} are refused too.
  • Leaving, withdrawing, confirming an issued link, erasing and pruning keep working when a setting they only pass by does not read (privacy, guards or client IP header on token links, the email normalizer sweep over other lists, the catalog behind a redirect, link rate limits, which fall back to 10 and 600): the mistake is reported to the logs instead. The signup, the gate and the reports stay strict.
  • Personal data is always encrypted with Laravel's encrypted casts. When rotating APP_KEY, keep the old key in APP_PREVIOUS_KEYS, then run php artisan waitlist:rekey before retiring it.
  • The package's models follow Laravel's encrypter, Model::encryptUsing() included. A key for the package only goes in a service provider's boot(): Waitlist::encryptUsing($encrypter) with any Illuminate\Contracts\Encryption\Encrypter. Its keys also make the lookup hash, so they must stay stable.
  • Supported extension points: your own models, the contracts with their config keys (catalog, url_generator, project_resolver, email_normalizer, spam_protector), container bindings, the useWaitlist gate, events and macros. A binding names a class that implements the contract, or an interface or abstract class your app binds; the contract itself and a class the container cannot build are refused. Other public classes (actions, SubscriptionLifecycle, controllers, the default implementations) are open but promise nothing, and a subclass only takes effect where the package resolves the class from the container or the config, not where it creates it with new. Guide: https://github.com/Taldres/laravel-waitlist/blob/main/docs/extending.md
  • php artisan waitlist:privacy prints the facts for the record of processing.
  • The operator is responsible for lawful processing, valid consent, notices, justified retention, infrastructure security and provider agreements. The package offers no legal advice, certification or GDPR compliance warranty; refer to https://github.com/Taldres/laravel-waitlist/blob/main/docs/responsibility.md and the MIT License, subject to mandatory law.
  • Retention defaults are not legal recommendations. Confirmed active entries and remaining reporting rows do not expire automatically. Review backups, queues, logs, exports and providers separately; remaining activity is not guaranteed anonymous.
  • waitlist:privacy is an incomplete technical inventory, not a complete Art. 30 record or compliance check. The package ships no application legal texts.

Related skills

This skill covers the integration as a whole. For a focused task, load:

  • laravel-waitlist-frontend: signup form, confirm, unsubscribe and preference pages, CORS, bot checks
  • laravel-waitlist-mail: confirmation, preference link, welcome, launch and newsletter mails
  • laravel-waitlist-provider-sync: Brevo, Mailchimp, Mailcoach in step, both ways
  • laravel-waitlist-projects: several products in one app, resolvers, a central API
  • laravel-waitlist-reporting: daily series, totals, the confirmed count over time
  • laravel-waitlist-launch: invitations in batches, launch mail, erasing the list afterwards
  • laravel-waitlist-privacy-operations: access and erasure requests, retention, key rotation, go-live check
  • laravel-waitlist-testing: tokens from events, mail assertions, HTTP route tests

Examples

  • "Add a waitlist to the landing page": define a list with its primary purpose, render Waitlist::purposes() in the form, call add() with the versions shown, and send the confirmation mail from an EntrySubscribed listener.
  • "Sync confirmed signups to Brevo": listen to EntryConfirmed, ConsentGranted, ConsentWithdrawn, EntryUnsubscribed and EntryForgotten, and use $entry->purposes to pick provider lists.
  • Testing: Event::fake([EntrySubscribed::class]), subscribe, then Event::assertDispatched(EntrySubscribed::class).

Anti-patterns

  • Writing to waitlist_subscriptions, waitlist_consents or waitlist_activity directly.
  • Posting consent text from the client, or bundling two purposes into one checkbox.
  • Mailing for a purpose without recipients(), whereConsentedTo() or hasConsentFor().
  • Changing state on GET or HEAD; confirm, unsubscribe, export and erase are POST.
  • Querying email in SQL; addresses are encrypted, use forEmail().
  • Putting a manage link (Waitlist::manageLink()) into ordinary mails; they carry the unsubscribe link, and the manage link is mailed only on request.

Individual skills in this repo

This repo contains 13 individual skills — each has its own dedicated page.

Taldres/laravel-waitlist

Use this skill when reviewing Laravel package compatibility across composer constraints, PHP versions, Laravel versions, Testbench versions, dependency stability lanes, Windows CI, or matrix-sensitive code and workflow changes.

Taldres/laravel-waitlist

Use this skill when creating or updating the bundled Laravel Boost skill under resources/boost/skills from the package implementation and package documentation. Trigger after public APIs, commands, config, routes, views, publish tags, README content, or examples change.

Taldres/laravel-waitlist

Use this skill when preparing Laravel package releases: CHANGELOG.md updates, generated release notes, GitHub release workflows, version checks, tags, release validation, or release automation changes. Never publish autonomously.

Taldres/laravel-waitlist

Use this skill when adding Laravel package capabilities or wiring them through the service provider: commands, migrations, routes, config merges, views, translations, assets, middleware, publish tags, workbench files, or console-only behavior.

Taldres/laravel-waitlist

Use this skill when writing, editing, fixing, or reviewing package tests with Pest 4/5 and Orchestra Testbench, including TDD, feature tests, unit tests, type coverage, arch tests, workbench behavior, commands, routes, config, migrations, and publishable resources.

Taldres/laravel-waitlist

Build the pages of a taldres/laravel-waitlist integration: the signup form with the registered wording, and the confirm, unsubscribe and preference pages, either in Laravel controllers (Blade, Livewire, Inertia) or in an SPA or static site over the package's JSON API, including CORS and bot protection.

Taldres/laravel-waitlist

Turn a taldres/laravel-waitlist waitlist into a launch: invite people in batches in signup order, let invited addresses register, announce the launch to everyone who agreed, and erase the list once its purpose is fulfilled.

Taldres/laravel-waitlist

Send the mails of a taldres/laravel-waitlist integration: the double opt-in confirmation, the preference page link, a welcome mail, launch mails and newsletters to the people who agreed, with the right unsubscribe links, one-click headers and a sender per project.

Taldres/laravel-waitlist

Operate a taldres/laravel-waitlist installation: answer access and erasure requests, withdraw a purpose on request, apply retention, erase a list once its purpose is fulfilled, rotate APP_KEY with waitlist:rekey, export a list, and check the setup before going live.

Taldres/laravel-waitlist

Run the waitlists of several products in one Laravel app with taldres/laravel-waitlist projects: define projects, scope facade calls with project(), resolve the project of HTTP signups, send mail per project, handle requests across all projects, or keep projects in a database for a central waitlist API.

Taldres/laravel-waitlist

Keep a newsletter tool such as Brevo, Mailchimp or Mailcoach in step with a taldres/laravel-waitlist waitlist: add confirmed contacts per purpose, remove them on withdrawal and erasure, and carry unsubscribes made at the provider back into the waitlist via a webhook.

Taldres/laravel-waitlist

Build waitlist statistics with taldres/laravel-waitlist: signups and confirmations per day for charts, totals and confirmation rates for a period, the confirmed count on any past day, and how many people are on a list right now, for dashboards, admin pages and API endpoints.

Taldres/laravel-waitlist

Write feature tests for an application's taldres/laravel-waitlist integration: get the plain tokens from events, assert the confirmation and other mails, test the signup, confirm and unsubscribe flows in PHP and over the package's HTTP routes, and bypass rate limits and bot checks in tests.

Skills relacionados