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

Mails Laravel Package

backstage/mails

Backstage Mails is a Laravel package for creating and managing dynamic email templates. Build reusable mail layouts, pass data into them, preview messages, and update transactional emails without redeploying your application.

View on GitHub
Deep Wiki
Context7

Getting Started

The shortest path to value is on a single non-production environment, not on every environment at once.

composer require backstage/mails
php artisan vendor:publish --tag="mails-config"      # verify tag names in the README
php artisan vendor:publish --tag="mails-migrations"
php artisan migrate

Then register the plugin in your Filament panel provider:

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            \Backstage\Mails\MailsPlugin::make(),
        ]);
}

Where to look first, in this order:

  1. composer.json of the package — confirm the filament/filament constraint covers your installed Filament version. As of v3.0.15 the package targets the ^5.0 line (and v3.0.15 specifically exists to fix Filament v5.7.0+). If you're still on Filament v3 or v4, you're on an older package major — check before you install.
  2. config/mails.php — this tells you what the package actually captures (recipients, body, headers, attachments) and what the master enable switch is called. Read it before writing any code.
  3. The published migration — the schema is the honest documentation. It tells you whether HTML/text bodies are stored, whether attachments are on disk or in the DB, and what indexes exist. Everything you do downstream (pruning, redaction, querying) depends on this.
  4. The Filament resource inside the plugin — how it filters, what actions it exposes (preview, resend, download attachment), and which policy/gate it checks. You will eventually want to override or restrict it.
  5. The listeners/service provider — confirm how mail is captured. Most packages in this space hook Laravel's MessageSending / MessageSent / MessageFailed events, which means capture happens regardless of transport. Verify that, because it changes your testing strategy entirely.

First use case: stop MAIL_MAILER=log from being a tail -f storage/logs/laravel.log exercise. Set your staging environment to the log/array mailer (or just a sandbox SMTP), trigger a password-reset, order confirmation, and invoice mail, and inspect the rendered HTML, headers, and attachments in the panel. That's the 15-minute win: QA can verify templates without a real inbox and without asking an engineer to read a log file.

Implementation Patterns

Gate it by environment, explicitly. Don't rely on the package's default. Force the plugin off in production unless you have a policy for it, and put the guard in the panel provider where it's visible:

->plugins(array_filter([
    ! app()->isProduction() ? MailsPlugin::make() : null,
]))

Treat the panel as a debugging tool, not an audit log. If you need a compliance-grade record of outbound mail, write your own append-only table and let this package stay a developer convenience. The two requirements diverge fast — audit logs need immutability and retention guarantees that a Filament CRUD resource doesn't give you.

Template iteration loop. This is where the package earns its keep. Edit the Blade mailable → trigger it via a Tinker one-liner or a seeded test route → refresh the panel → inspect. Keep a tiny artisan command in your app (php artisan mail:preview OrderConfirmation) that fires a mailable with fixture data. That command plus this panel removes the entire "send it to yourself and check Gmail" cycle.

Attachments and the storage disk. If attachments are stored on a disk, point that disk at something ephemeral in non-production and make sure the panel's download action is authorized. Large attachments are the usual cause of a bloated DB or a full disk; check the migration to see which storage strategy you got.

Restrict access with a policy or gate. The resource is registered by a third-party plugin, so authorization is easy to forget. Add a viewAny gate on the underlying model, or override the resource's canViewAny() / canAccess(). At minimum: admins and developers only — the stored bodies contain password-reset links, magic-login tokens, and customer PII.

Multi-tenant panels. If your Filament panel is scoped to a tenant, decide whether mail should be scoped too. A globally-registered resource will leak cross-tenant mail into a tenant panel. Either override the resource's query with a tenant scope or only register the plugin on your central/admin panel.

Queue workers. Mail sent from a queued job is still captured, provided the listeners are registered in the app's service provider (they are, if the package auto-discovers). This means a failing job and its captured mail are both visible — useful when debugging "the customer says they never got the receipt." Correlate by timestamp and recipient rather than expecting an automatic job-id link.

Testing. For unit tests, keep Mail::fake(). For feature tests where you want to assert on the rendered output, capture the mailable, render it yourself, and assert on the string — don't couple your test suite to this package's tables. If you do assert on log rows, wrap those assertions in a trait so a future package swap is one file.

Resend workflows. If the panel offers a resend action, it's genuinely useful for support ("customer deleted the invoice email") but it re-fires a real send. Verify what mailable class and payload it reconstructs and whether it respects your queue configuration — a synchronous resend from the panel can time out on a slow SMTP provider. Consider overriding the action to dispatch a job instead.

Pin the package, and stage the Filament upgrade — not the other way around. The package subclasses Filament internals (ListMails extends BasePage and overrides unmountAction()), so a Filament minor bump can break it at discovery time. When you bump filament/filament, treat the following composer update backstage/mails as part of the same change, in the same PR, on a branch where php artisan about runs cleanly. Don't let those two drift.

Gotchas and Tips

A fatal at package discovery means a Filament signature drift, not a bug in your code. v3.0.15 exists purely because Filament v5.7.0 widened InteractsWithActions::unmountAction() from bool $canCancelParentActions = true to bool|string|null $cancelParentActions = null. The narrower override in ListMails stopped matching and Laravel died before booting with:

Declaration of ...ListMails::unmountAction(bool $canCancelParentActions = true): void
must be compatible with Filament\Pages\BasePage::unmountAction(string|bool|null $cancelParentActions = null): void

If you see must be compatible with pointing at a vendor Filament page class, run composer update backstage/mails first — no code changes needed. The v3.0.15 override accepts the widest type and forwards only the arguments it actually received, so it now works across the whole filament/filament: ^5.0 range, including the new "cancel up to a named parent action" string form.

Version-compatibility is the first thing to check. Filament v3 → v4 was a breaking change for essentially every plugin, and the v4 → v5 line is the current target. Confirm the package's composer.json constraints against your Filament version before you install, and pin the package version in composer.json rather than floating — with 163 stars and a single maintainer, an unpinned range is a real risk on a composer update day. The discovery-time fatal above is exactly the kind of failure a lockfile saves you from.

Migrations must be published before you migrate. If you run migrate first and the service provider's migrations are disabled or the tag name differs, you get a confusing "table not found" from the panel. Publish, read the file, then migrate.

Low dependents means low blast radius of other people's bug reports. You are the early adopter. Budget time for reading src/ when something behaves unexpectedly, and prefer wrapping the package behind your own thin service (a MailLog repository, a custom Filament page) so replacing it later isn't a rewrite. Given dependents: 0, treat it as "a well-made package I'm adopting" rather than "a de facto standard."

Nothing appears in the panel — the checklist:

  • Did the app boot at all? php artisan about — a discovery-time signature fatal from a Filament bump looks like "nothing works," not "nothing appears."
  • Is the plugin registered on this panel (not just another panel provider)?
  • Is capture enabled in config, and does the config cache need clearing (php artisan config:clear)?
  • Are the event listeners registered? php artisan event:list | grep -i mail.
  • Did the mail actually send? Check failed_jobs — a queued mailable that never ran was never captured.
  • Is the resource hidden by authorization? Temporarily dd() in the resource's query.

Table growth is the silent failure mode. Logged mail tables accumulate bodies and attachment paths fast in staging. Add a pruning schedule early — schedule a cleanup command or use Prunable on your own model wrapper. At minimum, a nightly DELETE FROM mails WHERE created_at < now()->subDays(14) on non-production, plus an index on created_at. Check whether the package ships a prune command — if it does, use it rather than hand-rolling SQL.

PII redaction is not optional in shared environments. Password reset links grant account access. If staging is reachable by contractors or QA, either restrict the panel hard or add a redaction step (custom accessor, or a view override) that masks the body for anything matching a reset/magic-link signature.

Extension points to know about: the config file (enable/disable, disk, retention), the Filament resource (most UI tweaks mean subclassing the plugin's page/resource or overriding a view), and the model (if it's configurable, you can swap in your own with extra scopes — check for a models.mail config key). Publishing views is the escape hatch for cosmetic changes; prefer that over forking the package. Note that subclassing a page is what pulls you into the unmountAction() signature surface, so keep those overrides shallow and re-test them on every Filament bump.

Don't let this become your production mail monitor. If someone asks "can we keep this on in prod so support can look up emails," that's a feature request for a proper outbound-mail audit system with access controls, retention policy, and a documented data-processing note — not a Filament plugin. Say that out loud early; it saves you from a security review later.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource