lacodix/laravel-scoped-mail-config
Send Laravel mail with dynamic, per-model mail configuration. Implement HasMailConfig on tenants, teams, users, or any model to scope SMTP and sender settings—ideal for multi-tenancy and admin-managed mail configs.
Architecture fit
Mail facade: a HasMailConfig contract on a scope model (Tenant/Team/User) plus a facade that resolves mail configuration at send time. Surface area is small, so vendor lock-in is low — if it's abandoned we can vendor or reimplement the few classes involved.Lacodix\LaravelScopedMailConfig\Concerns\HasMailConfig but declares implements HasMailConfig. Whether the contract is an interface, a trait, or both must be confirmed in source before anyone estimates work against it.Integration feasibility
composer require, one line in AppServiceProvider (ScopedMail::resolveScopeUsing(...)), one getMailConfig($name): array method. A half-day spike is realistic.Mail:: to ScopedMail::. That touches notifications, queued mailables, password resets, invites, and any third-party package (Fortify, Breeze, Cashier, Nova) that sends through the default facade — those keep using the global config silently and invisibly.Technical risk
handle(), this package is a demo, not a production system.mail.* keys.ScopedMail::fake() only covers the new facade. Existing Mail::fake() assertions will not catch an unscoped send, so scope regressions can ship green.Key questions
Stack fit — Best fit is a Laravel 10/11/12 app already on the Mail facade with a first-class Tenant model, credentials columns, and existing tenant-resolution middleware. Poor fit if most mail is dispatched from queued jobs with no tenant handle, or if we run serverless/Vapor with tight cold-start budgets.
Migration path
Tenant, wire resolveScopeUsing, and send to a sandbox inbox (Mailpit/Mailtrap).TenantMail::send()) rather than letting call sites bind directly to the package facade — makes fork-and-replace a one-file change.Compatibility — Verify against our exact Laravel minor and PHP version; confirm behavior under config:cache and optimize. Test the synchronous path and the queued path independently, and check that ScopedMail::fake() coexists with existing Mail::fake() expectations. If we run Octane, add a concurrency test for cross-request config bleed.
Sequencing — (1) synchronous transactional mail for one pilot tenant; (2) admin self-serve credentials UI with save-time validation and a test send; (3) queued mail once scope rehydration is proven; (4) notifications and third-party package paths; (5) additional providers only after 1–3 are stable. Budget roughly 2–3 developer-days for integration and 2–4 weeks for production hardening (queue semantics, secrets, observability, deliverability plumbing).
Maintenance — Low ceremony and low dependency weight, but effectively self-maintained. Pin the version, add a canary job against the next Laravel major, and encode the expected contract in our own tests so an upstream signature change fails loudly rather than silently.
Support — No vendor support, no security policy, one maintainer. We need an internal runbook: how to inspect a tenant's effective mail config, rotate a credential, and force-fallback a tenant to global. The package also provides no per-send audit trail, so we must log tenant id, transport, and from-address on every send ourselves.
Scaling — Config resolution is negligible next to an SMTP handshake, but per-tenant credentials multiply outbound connections and provider accounts. Connection pooling, per-tenant rate limits, and provider-side quotas (SES sandbox, per-domain sending caps) become real constraints and tend to surface at the worst moment. Credential storage grows linearly with tenants — plan key management and rotation early.
Failure modes
| Failure | Blast radius | Mitigation |
|---|---|---|
| Missing/unresolvable scope | Silent wrong identity or global fallback | Fail-closed policy plus alerting; assert scope present in the send path |
| Queued job without tenant context | Wrong tenant's credentials used | Serialize tenant id on the mailable; rehydrate in handle() |
| Octane/worker config bleed | Cross-tenant mis-send | Per-send resolution, no global mutation, concurrency test |
| Tenant submits bad SMTP | Mail loss for that tenant only | Validate on save plus a test message; surface the error in the UI |
| Credential compromise | That tenant's provider account | Encrypt at rest, rotate, least-privilege keys, audit log |
| Package abandonment | Migration cost | MIT fork; our wrapper keeps call sites unchanged |
| Deliverability/reputation | Shared domain blacklisting | Per-tenant subdomain or domain, DKIM/SPF onboarding, bounce webhooks |
Ramp-up — A Laravel developer is productive in half a day from the README. The real ramp-up is tenancy and deliverability context: who the tenant is, which provider they use, and who controls their DNS. Assign a named owner for tenant email onboarding — that workflow, not the package, is the recurring operational cost. Recommendation: proceed to a timeboxed spike, but gate production on queue-context correctness, encrypted credentials, and per-send observability, and treat the dependency as vendorable code rather than a supported product.
How can I help you explore Laravel packages today?