Double Opt-In Newsletter with Mailtrap in Laravel
Most newsletter services handle subscriptions on their end. You embed a form, they manage the double opt-in, you get a list of confirmed subscribers. But if you want control over the flow — custom confirmation emails, your own database records, integration with your existing user system — you need to build it yourself. I did, and packaged it as pulli/laravel-mailtrap-subscriber.
Why Mailtrap?
Mailtrap started as a testing tool for catching emails in development. Their sending API is a more recent addition, and it's surprisingly good:
- Clean REST API with good documentation
- Transactional and bulk email from the same platform
- Contact management with custom attributes
- Generous free tier for small projects
- No per-subscriber pricing — you pay for sends, not list size
I was already using Mailtrap for email testing in development. Using their sending API for the newsletter meant one less service to manage.
How double opt-in works
The flow is straightforward:
- User submits their email address
- App creates a subscriber record with a unique confirmation token
- App sends a confirmation email with a link containing the token
- User clicks the link
- App marks the subscriber as confirmed and syncs them to Mailtrap's contact list
- Admin gets notified of the new subscriber
If the user never clicks the link, the subscriber stays unconfirmed and never gets added to the mailing list. This is a legal requirement in the EU (GDPR) and a best practice everywhere else.
The database model
The package creates a mailtrap_subscribers table with the essentials:
Schema::create('mailtrap_subscribers', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->string('token')->unique();
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
});
The token is a random string generated at subscription time. confirmed_at stays null until the user clicks the confirmation link.
Sending the confirmation email
The confirmation email is a standard Laravel Mailable. The package sends it through your configured mail driver — which should be Mailtrap's SMTP or API transport for production:
<?php
// Simplified — the actual package Mailable
class ConfirmSubscription extends Mailable
{
public function __construct(
public MailtrapSubscriber $subscriber
) {}
public function content(): Content
{
return new Content(
markdown: 'mailtrap-subscriber::emails.confirm',
with: [
'url' => route('mailtrap-subscriber.confirm', $this->subscriber->token),
],
);
}
}
Confirmation and syncing
When the user clicks the confirmation link, the package verifies the token, marks the subscriber as confirmed, and syncs them to Mailtrap's contact list via the API:
// The confirmation route handler
public function confirm(string $token): RedirectResponse
{
$subscriber = MailtrapSubscriber::where('token', $token)
->whereNull('confirmed_at')
->firstOrFail();
$subscriber->update(['confirmed_at' => now()]);
// Sync to Mailtrap contact list
MailtrapApi::addContact($subscriber->email);
return redirect('/')->with('success', 'Subscription confirmed!');
}
Using it with Livewire
On my site, the newsletter form is a Livewire component. The submit method creates the subscriber and sends the confirmation email. Flux handles the UI and toast notification:
public function subscribe(): void
{
$this->validate([
'email' => ['required', 'email', 'unique:mailtrap_subscribers,email'],
]);
$subscriber = MailtrapSubscriber::create([
'email' => $this->email,
'token' => Str::random(64),
]);
Mail::to($subscriber->email)->send(new ConfirmSubscription($subscriber));
$this->reset('email');
Flux::toast('Check your inbox to confirm your subscription.');
}
Why a package?
I extracted this into a package because I use the same newsletter setup on multiple sites. The package publishes the migration, views, and config. Each site customizes the confirmation email template and redirect URL, but the subscription flow is identical. One composer require, one migration, done.