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-privacy-operations
laravel-waitlist-privacy-operations
taldres/laravel-waitlist
claude-code
privacy
data-retention
erasure
consent
personal-data
data-subject-requests

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, import existing subscribers with waitlist:import, and check the setup before going live.

Install
php artisan boost:add-skill taldres/laravel-waitlist

Save this content to: .claude/skills/laravel-waitlist-privacy-operations/SKILL.md

# Laravel Waitlist: privacy operations

Use this skill for day-to-day data protection tasks around `taldres/laravel-waitlist`:
requests from people on the list, retention, key rotation, exports and the go-live
check.

## Primary Goal

- carry out each request with the package's own APIs and commands, across every
  project where the request concerns the person, and say what stays outside the package

## Workflow

### Access request

```bash
php artisan waitlist:show [email protected] --pretty   # every project; --json for a machine-readable copy
```

```php
Waitlist::allProjects()->personalData('[email protected]'); // Collection<PersonalData>
```

### Erasure request

```bash
php artisan waitlist:forget [email protected]          # every project
```

```php
Waitlist::allProjects()->forget('[email protected]');  // number of entries erased
```

Erasure deletes the address, its cycles and consents, and clears identifying
fields in its log rows; it fires `EntryForgotten` per entry, which listeners use
to delete copies at providers. Backups, queues, logs, exports and provider copies
need separate handling.

### Withdrawal that arrives by mail

```php
Waitlist::for('beta')->withdraw('[email protected]', 'newsletter'); // optional purpose: every list of the project
Waitlist::for('beta')->unsubscribe('[email protected]');            // leave this list
```

### Erase a list once its purpose is fulfilled

```bash
php artisan waitlist:forget --list=beta --all          # asks first; --force without a terminal
```

```php
Waitlist::for('beta')->forgetAll();
```

### Retention

`waitlist:prune` runs on your schedule, which the package does not set:
`Schedule::command('waitlist:prune')->daily()` in `routes/console.php`, with
Laravel's scheduler running (`php artisan schedule:run` every minute);
`waitlist:privacy` warns while no scheduled task runs it. Periods in `waitlist.retention`: `pending_days` (30), `unsubscribed_days`
(1095), `request_metadata_days` (30); `null` keeps data, while a period reaching
back before 1970 (a 36500-day "forever") is refused. `waitlist:prune` applies every
period that reads, then reports the ones that do not and fails the run: watch the
logs and the exit code. These are technical defaults, not legal recommendations.
`reconfirm_after_days` (`null`, off) asks confirmed addresses that have been
quiet that long to confirm again (`ReconfirmationRequested`, mailed by your
listener); without an answer within `reconfirm_within_days` (30) they leave the
list as `lapsed` and are kept like anyone who left. Without it, active confirmed
entries do not expire; the remaining reporting rows never do.

### Rotate `APP_KEY`

1. Put the old key into `APP_PREVIOUS_KEYS`, set the new `APP_KEY`, deploy.
2. `php artisan waitlist:rekey`
3. Remove the old key once no queued job or backup needs it.

Without the old key, addresses can neither be read nor found by email, and each
unsubscribe token is replaced the next time it is needed, so links sent earlier
stop working from then on.

### Export

```bash
php artisan waitlist:export beta --status=confirmed --path=storage/app/beta.csv
```

Columns come from `waitlist.export.columns`, limited to `CsvExporter::EXPORTABLE`
(no tokens). The file holds addresses in plain text; delete it when done.

### Import existing subscribers

```bash
php artisan waitlist:import subscribers.csv --source=mailchimp-2026-09 --list=beta --with-consent
php artisan waitlist:import subscribers.csv --source="paper forms" --list=beta --request-confirmation --limit=500
```

CSV columns: `email`, `purposes` (`waitlist:2025-01, newsletter:2025-01`), and
optionally `list`, `locale`, `consented_at`. `--with-consent` confirms without a
mail and keeps `consented_at` as the time of each consent (`EntryImported` fires);
`--request-confirmation` leaves each address pending, and `EntrySubscribed` asks
for the confirmation mail. Only registered wording versions are accepted. The
source is stored encrypted in the `imported` log step, shown by `waitlist:show` and
listed by `waitlist:privacy`. Addresses already on the list, in any status, are
skipped; a refused row is reported by line and column, never by a value. `--limit`
stops and prints the resume command (`--from-line=N`); never rerun from the start,
which brings back every address erased since. Delete the file afterwards.
From PHP: `Waitlist::for($list)->import($email, $purposes, source: ..., consentedAt: ...)`.

### Record of processing

```bash
php artisan waitlist:privacy [--project=]
```

It describes configured package storage, purposes, the sources of imported
entries still held, the metadata fields of each list, retention and listeners
registered for package events. It is input, not a
complete record: it does not audit infrastructure or find every recipient.

### Go-live check

- `APP_KEY` backed up apart from the database
- double opt-in on; IP and user agent off unless needed
- fields per project and list (`->fields()`) as few as possible, none if in doubt
- `waitlist:prune` scheduled and the scheduler running; retention periods chosen and justified
- `php artisan waitlist:check` passes on the production host (all settings read, no
  setting the package does not read, no table or column missing)
- `php artisan waitlist:privacy` run on the production host prints no warnings (mail
  links https and not localhost, `APP_DEBUG` off, guests meant to call the signup)
- config reads: no empty `WAITLIST_*=` line in `.env` (an empty value is refused;
  delete the line for the default), `php artisan config:cache` run again after a
  package update (and `route:cache` after a routes setting changed), no
  `InvalidConfigurationException` in the logs
- signup form renders `Waitlist::purposes()` or `GET /waitlist/purposes`; optional
  purposes unticked
- confirmation listener records `Waitlist::confirmationMailed()`
- every mail has a per-purpose unsubscribe link, and one-click headers where the provider accepts them
- `ManageLinkRequested` mailed only to the address
- queued listeners implement `ShouldBeEncrypted`; `EntryForgotten`,
  `EntryUnsubscribed` and `ConsentWithdrawn` reach every provider
- bot protection and trusted proxies configured when the routes are on

## Rules, References, and Templates

- Commands without `--project` cover every project for `show`, `forget`, `prune`
  and `privacy`; a `--list` without `--project` means that list of the default
  project.
- Withdrawing, leaving, erasing and pruning do not wait for settings they only pass
  by (privacy, guards or client IP header on token links, the email normalizer's
  sweep over the address's other lists, the catalog behind a redirect, link rate
  limits): they fall back and report the mistake once per request, job or
  command, so read the logs after a config change.
- The operator is responsible for lawful processing, identity checks, deadlines
  and complete answers; the package offers no legal advice or compliance
  warranty: https://github.com/Taldres/laravel-waitlist/blob/main/docs/responsibility.md
- Guides: https://github.com/Taldres/laravel-waitlist/blob/main/docs/gdpr.md,
  https://github.com/Taldres/laravel-waitlist/blob/main/docs/encryption-and-keys.md

## Examples

- "A user asked us to delete their data": verify the request, then
  `php artisan waitlist:forget <email>`, and check providers and exports.
- "We launched, the beta list has served its purpose": `waitlist:forget --list=beta --all`.
- "We rotated APP_KEY and lookups fail": restore the old key in
  `APP_PREVIOUS_KEYS`, then `waitlist:rekey`.

## Anti-patterns

- Deleting rows with SQL or `WaitlistEntry::query()->delete()`; erasure must go
  through the package so the log is stripped and `EntryForgotten` fires.
- Searching `where('email', ...)`: addresses are encrypted; use `forEmail()` or the
  facade.
- Erasing with `Waitlist::forget()` (default project only) for a person on several
  projects.
- Rotating `APP_KEY` without `APP_PREVIOUS_KEYS`.
- Presenting `waitlist:privacy` output as a complete Art. 30 record.
- Importing with `--with-consent` and a made-up `consented_at`, or `add()` in a loop:
  the first fakes a consent record, the second treats known addresses as resends.

Laravel Waitlist: privacy operations

Use this skill for day-to-day data protection tasks around taldres/laravel-waitlist: requests from people on the list, retention, key rotation, exports and the go-live check.

Primary Goal

  • carry out each request with the package's own APIs and commands, across every project where the request concerns the person, and say what stays outside the package

Workflow

Access request

php artisan waitlist:show [email protected] --pretty   # every project; --json for a machine-readable copy
Waitlist::allProjects()->personalData('[email protected]'); // Collection<PersonalData>

Erasure request

php artisan waitlist:forget [email protected]          # every project
Waitlist::allProjects()->forget('[email protected]');  // number of entries erased

Erasure deletes the address, its cycles and consents, and clears identifying fields in its log rows; it fires EntryForgotten per entry, which listeners use to delete copies at providers. Backups, queues, logs, exports and provider copies need separate handling.

Withdrawal that arrives by mail

Waitlist::for('beta')->withdraw('[email protected]', 'newsletter'); // optional purpose: every list of the project
Waitlist::for('beta')->unsubscribe('[email protected]');            // leave this list

Erase a list once its purpose is fulfilled

php artisan waitlist:forget --list=beta --all          # asks first; --force without a terminal
Waitlist::for('beta')->forgetAll();

Retention

waitlist:prune runs on your schedule, which the package does not set: Schedule::command('waitlist:prune')->daily() in routes/console.php, with Laravel's scheduler running (php artisan schedule:run every minute); waitlist:privacy warns while no scheduled task runs it. Periods in waitlist.retention: pending_days (30), unsubscribed_days (1095), request_metadata_days (30); null keeps data, while a period reaching back before 1970 (a 36500-day "forever") is refused. waitlist:prune applies every period that reads, then reports the ones that do not and fails the run: watch the logs and the exit code. These are technical defaults, not legal recommendations. reconfirm_after_days (null, off) asks confirmed addresses that have been quiet that long to confirm again (ReconfirmationRequested, mailed by your listener); without an answer within reconfirm_within_days (30) they leave the list as lapsed and are kept like anyone who left. Without it, active confirmed entries do not expire; the remaining reporting rows never do.

Rotate APP_KEY

  1. Put the old key into APP_PREVIOUS_KEYS, set the new APP_KEY, deploy.
  2. php artisan waitlist:rekey
  3. Remove the old key once no queued job or backup needs it.

Without the old key, addresses can neither be read nor found by email, and each unsubscribe token is replaced the next time it is needed, so links sent earlier stop working from then on.

Export

php artisan waitlist:export beta --status=confirmed --path=storage/app/beta.csv

Columns come from waitlist.export.columns, limited to CsvExporter::EXPORTABLE (no tokens). The file holds addresses in plain text; delete it when done.

Import existing subscribers

php artisan waitlist:import subscribers.csv --source=mailchimp-2026-09 --list=beta --with-consent
php artisan waitlist:import subscribers.csv --source="paper forms" --list=beta --request-confirmation --limit=500

CSV columns: email, purposes (waitlist:2025-01, newsletter:2025-01), and optionally list, locale, consented_at. --with-consent confirms without a mail and keeps consented_at as the time of each consent (EntryImported fires); --request-confirmation leaves each address pending, and EntrySubscribed asks for the confirmation mail. Only registered wording versions are accepted. The source is stored encrypted in the imported log step, shown by waitlist:show and listed by waitlist:privacy. Addresses already on the list, in any status, are skipped; a refused row is reported by line and column, never by a value. --limit stops and prints the resume command (--from-line=N); never rerun from the start, which brings back every address erased since. Delete the file afterwards. From PHP: Waitlist::for($list)->import($email, $purposes, source: ..., consentedAt: ...).

Record of processing

php artisan waitlist:privacy [--project=]

It describes configured package storage, purposes, the sources of imported entries still held, the metadata fields of each list, retention and listeners registered for package events. It is input, not a complete record: it does not audit infrastructure or find every recipient.

Go-live check

  • APP_KEY backed up apart from the database
  • double opt-in on; IP and user agent off unless needed
  • fields per project and list (->fields()) as few as possible, none if in doubt
  • waitlist:prune scheduled and the scheduler running; retention periods chosen and justified
  • php artisan waitlist:check passes on the production host (all settings read, no setting the package does not read, no table or column missing)
  • php artisan waitlist:privacy run on the production host prints no warnings (mail links https and not localhost, APP_DEBUG off, guests meant to call the signup)
  • config reads: no empty WAITLIST_*= line in .env (an empty value is refused; delete the line for the default), php artisan config:cache run again after a package update (and route:cache after a routes setting changed), no InvalidConfigurationException in the logs
  • signup form renders Waitlist::purposes() or GET /waitlist/purposes; optional purposes unticked
  • confirmation listener records Waitlist::confirmationMailed()
  • every mail has a per-purpose unsubscribe link, and one-click headers where the provider accepts them
  • ManageLinkRequested mailed only to the address
  • queued listeners implement ShouldBeEncrypted; EntryForgotten, EntryUnsubscribed and ConsentWithdrawn reach every provider
  • bot protection and trusted proxies configured when the routes are on

Rules, References, and Templates

Examples

  • "A user asked us to delete their data": verify the request, then php artisan waitlist:forget <email>, and check providers and exports.
  • "We launched, the beta list has served its purpose": waitlist:forget --list=beta --all.
  • "We rotated APP_KEY and lookups fail": restore the old key in APP_PREVIOUS_KEYS, then waitlist:rekey.

Anti-patterns

  • Deleting rows with SQL or WaitlistEntry::query()->delete(); erasure must go through the package so the log is stripped and EntryForgotten fires.
  • Searching where('email', ...): addresses are encrypted; use forEmail() or the facade.
  • Erasing with Waitlist::forget() (default project only) for a person on several projects.
  • Rotating APP_KEY without APP_PREVIOUS_KEYS.
  • Presenting waitlist:privacy output as a complete Art. 30 record.
  • Importing with --with-consent and a made-up consented_at, or add() in a loop: the first fakes a consent record, the second treats known addresses as resends.
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.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names