4 · Email infrastructure
Objective — confirm and test the transactional mail path, choose a provider from the deliverability matrix, customize the core template set, and (optionally) add drip campaigns and a newsletter — all with SPF/DKIM/DMARC passing — so launch-critical mail reliably reaches the inbox.
Background
Section titled “Background”Transactional email is launch-blocking: password resets, receipts, and verification mails must arrive in the inbox — not spam. This page is the canonical place to wire email end-to-end. DNS records (SPF/DKIM/DMARC) and the MAIL_* env vars are set in Phase 4; here you confirm, choose a provider, and prove delivery.
flowchart LR A["1. Confirm + test<br/>existing config"] --> B{"Inbox + passing<br/>auth?"} B -- Yes --> D["3. Templates"] B -- "No / spam" --> C["2. Provider setup"] C --> D D --> E["4. Drip campaigns<br/>(optional)"] E --> F["5. Newsletter<br/>(optional)"]1. Confirm the provider & test delivery (MUST)
Section titled “1. Confirm the provider & test delivery (MUST)”Most CodeCanyon apps ship with email already wired — verify what’s in place before adding a new provider.
-
Check the existing config.
Terminal window grep -E "^MAIL_MAILER|^MAIL_HOST|^MAIL_PORT|^MAIL_FROM" Admin-Local/1-Project/2-Env/.env.production 2>/dev/null# Expected: MAIL_HOST set and MAIL_MAILER not log/array → email is configuredIf
MAIL_HOSTis set andMAIL_MAILERisn’tlog/array, email is configured — go to step 3.- ✅ The
MAIL_*config is read and its state known.
- ✅ The
-
Check the admin-panel override. Many apps store mail settings in the DB, overriding
.env(Settings → SMTP,Config → Mail Configuration,System → Mail Settings). Confirm which layer wins.Terminal window php artisan tinker>>> config('mail.mailers.smtp.host')# Expected: the effective SMTP host (DB-override or .env)- ✅ You know whether the admin panel or
.envwins at runtime.
- ✅ You know whether the admin panel or
-
Send a real test, then verify it in the inbox.
Terminal window php artisan tinker>>> Mail::raw('Test on '.now(), fn($m) => $m->to('you@<domain>')->subject('Mail test'));# Expected: the test mail is dispatched without errorThen check the inbox (including spam), open the email source and confirm SPF, DKIM, and DMARC all show
pass, and trigger a real transactional mail via/password/reset.- ✅ The test email arrives in the inbox with
spf=pass/dkim=pass/dmarc=pass, and password reset works end-to-end.
- ✅ The test email arrives in the inbox with
-
If delivery fails or lands in spam — set up a dedicated provider (section 2). Shared-hosting SMTP works for low volume but deliverability degrades fast past ~50/day.
- ✅ A decision is made: keep the existing config, or move to section 2.
2. Set up a provider (MUST if delivery is weak)
Section titled “2. Set up a provider (MUST if delivery is weak)”Pick one transactional provider, authenticate the domain, then re-run the section-1 test.
-
Pick one transactional provider.
Provider Free tier Deliverability Best for Region SendGrid 100/day forever 99.95% Default choice, best docs, native Laravel driver Global, US Mailgun 5k/mo for 3 mo, then PAYG 99.9% Pay-per-email, developer-focused US + EU AWS SES 62k/mo free from EC2, else $0.10/1k 99% Highest volume, lowest cost at scale Multi-region Brevo 300/day forever 98% Newsletter + transactional in one EU, GDPR-friendly Hostinger Titan Included with hosting 95% Low volume, already on Hostinger ~50/day practical Resend 3k/mo, 100/day 99% Modern API + DX, React Email US, newer Decision criteria: starting out / global → SendGrid; EU-first / GDPR → Mailgun (EU) or Brevo; heavy on AWS, >10k/day → SES; already on Hostinger, <50/day → Titan; modern stack, love DX → Resend.
- ✅ One provider is chosen against the criteria.
-
Wire SendGrid (recommended default). Create the account at sendgrid.com, authenticate the domain (Settings → Sender Authentication → Authenticate Your Domain → add the 3 DKIM CNAMEs to DNS per Phase 4, then Verify), create a Restricted — Mail Send only API key (
<project>-production, shown once), then wire the env vars.Terminal window MAIL_MAILER=smtpMAIL_HOST=smtp.sendgrid.netMAIL_PORT=587MAIL_USERNAME=apikeyMAIL_PASSWORD=<SG.your-api-key>MAIL_ENCRYPTION=tlsMAIL_FROM_ADDRESS=noreply@<domain>MAIL_FROM_NAME="<App Name>"# Expected: domain shows green DKIM checks; env wired with the API key from the vault- ✅ The domain is authenticated and the SendGrid env vars are wired.
-
Re-run the section-1 test with the new provider wired.
- ✅ The test send succeeds with
spf=pass,dkim=pass,dmarc=pass.
- ✅ The test send succeeds with
If you chose a provider other than SendGrid, the domain-auth + key pattern is the same — only the specific records and env keys differ.
3. Customize the transactional templates (SHOULD)
Section titled “3. Customize the transactional templates (SHOULD)”Customize the core template set so every system email is on-brand.
-
Check the admin panel. Many apps ship a template editor (
Settings → Email Templates,Communication → Templates,Admin → Notifications → Email). Customize subjects, body, colors, and logo in the UI.- ✅ Templates are customized in the admin UI (if it exists).
-
Check DB seeders. Some apps store templates in an
email_templatestable.Terminal window ls database/seeders/ | grep -i -E "email|template|mail"php artisan db:seed --class=EmailTemplatesSeeder --force # after editing# Expected: a template seeder is found and reseeded- ✅ Template seeders are found and reseeded (if present).
-
Use code-based Markdown mail (no admin UI, no seeder).
Terminal window php artisan vendor:publish --tag=laravel-mail # edit resources/views/vendor/mail/html/*.blade.phpphp artisan vendor:publish --tag=cashier-views # edit resources/views/vendor/cashier/*.blade.php# Expected: the mail + cashier views are published for editing- ✅ The Markdown mail views are published for editing.
-
Customize the minimum template set — always customize these.
Template Trigger Critical fields Welcome Registration Logo, first-step CTA, support link Email verification Verify notification Signed URL that expires Password reset /password/resetSigned URL, 60-min expiry notice Invoice receipt invoice.payment_succeededAmount, plan, PDF link Renewal reminder Cashier 3-day-before hook Renewal date, cancel link Payment failed invoice.payment_failedRetry link, grace period, support - ✅ All six core templates are customized.
-
Apply brand consistency — logo via an absolute HTTPS URL (not
asset()), brand-colored primary button, footer with company name + physical address (CAN-SPAM) + unsubscribe link for marketing mail,<App Name>subject prefix where appropriate.- ✅ Triggering all six templates in staging renders correctly in Gmail, Outlook, and an iOS client, matching the brand kit.
4. Add email sequences / drip campaigns (OPTIONAL)
Section titled “4. Add email sequences / drip campaigns (OPTIONAL)”Wire automated sequences if the product benefits from onboarding/trial nudges.
-
Check the admin panel for a drip UI (
Automation → Sequences,Marketing → Email Sequences). If present, configure day 0/3/7/14 emails there.- ✅ Drip sequences are configured in the admin UI (if it exists).
-
Choose an approach.
Approach Cost Best for Laravel-native (Notification + scheduler) $0 Simple sequences; teams already in Laravel Mailchimp Automations Free ≤500 Non-technical, visual editor, high volume ConvertKit Sequences $0–15/mo Creators, course sales, opt-in funnels Encharge / Customer.io $49+/mo Product-led growth, complex branching Default to Laravel-native for transactional sequences (welcome, trial-ending, re-engagement); graduate to ConvertKit/Mailchimp when marketing outgrows developer capacity.
- ✅ A drip approach is chosen.
-
Wire the Laravel-native example (if chosen).
app/Console/Commands/SendTrialEndingDrip.php $users = User::whereHas('subscription', fn($q) => $q->where('trial_ends_at', '<=', now()->addDays(3)))->whereDoesntHave('notifications', fn($q) => $q->where('type', TrialEndingNotification::class))->get();foreach ($users as $user) { $user->notify(new TrialEndingNotification()); }Schedule it in
app/Console/Kernel.php:$schedule->command('drips:trial-ending')->dailyAt('10:00');- ✅ The drip command is wired and scheduled.
-
Wire the external handoff (if using ConvertKit/Mailchimp/Encharge) — tag users from your webhook handlers (sign up, upgrade, cancel, churn) so the external tool drives the drip.
- ✅ The first email sends manually, and users exit the sequence when they take the intended action (e.g. upgrade → skip remaining trial-ending mails).
5. Add a newsletter (OPTIONAL)
Section titled “5. Add a newsletter (OPTIONAL)”Wire subscriber capture if you’ll publish to users.
-
Check the admin panel for subscriber management (
Marketing → Subscribers,Communication → Mailing List).- ✅ The admin newsletter surface is used if it exists.
-
Choose a platform.
Platform Cost Double opt-in Best for Laravel-native ( spatie/laravel-newsletter+ Mailchimp)Free + Mailchimp free Yes Self-hosted signup forms Mailchimp embed Free ≤500 Yes Fastest setup, zero code ConvertKit embed $0–15/mo Yes Creators, editorial content Beehiiv Free ≤2,500 Yes Publication-style, monetization - ✅ A newsletter platform is chosen.
-
Wire the
spatie/laravel-newsletterapproach (if chosen).Terminal window composer require spatie/laravel-newsletterphp artisan vendor:publish --provider="Spatie\Newsletter\NewsletterServiceProvider"# Expected: the package installs and its config is publishedpublic function subscribe(Request $request): RedirectResponse{$request->validate(['email' => 'required|email']);Newsletter::subscribe($request->email);return back()->with('status', 'Subscribed! Check your inbox to confirm.');}- ✅ The subscribe endpoint is wired.
-
Apply compliance — double opt-in (GDPR/CAN-SPAM/CASL), unsubscribe link in every email, sender physical address in the footer, an explicit (not pre-checked) consent box, and a mention in the Phase 7 privacy policy’s “marketing communications” section.
- ✅ Signup creates a subscriber in the platform dashboard, the confirmation email arrives with a working opt-in link, and the unsubscribe link actually unsubscribes.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🔀 Test mail lands in the inbox — with
spf=pass,dkim=pass,dmarc=pass. - 👤 A dedicated provider is wired — if shared SMTP deliverability was weak.
- 🔀 The six core templates render — correctly in Gmail, Outlook, and iOS, on-brand.
- 🔀 Drip campaigns and newsletter configured — or consciously deferred, not silently skipped.