laravel-notification-channels/discord
Send Discord messages from your Laravel app through the notification system. This channel lets you post rich embeds, files, and webhook messages using Laravel's familiar notification classes.
Architecture fit — good, with a hard scope boundary. This is a thin adapter into Laravel's existing notification pipeline: via() returns DiscordChannel::class, the notifiable exposes routeNotificationForDiscord(), and toDiscord() returns a DiscordMessage whose toArray() is literally the Discord webhook JSON body. There is no new runtime component to own — no bot process, no gateway connection, no Discord app registration, no inbound endpoint. That maps cleanly onto any roadmap item shaped like "tell a channel that something happened." It maps poorly onto anything a stakeholder might assume that implies: this is outbound, one-way, webhook-only. No replies, reactions, edits, threads, slash commands, or presence. Interactive payloads (buttons, select menus) are described as supported, but handling an interaction is a different package, different hosting, different permissions. Correct that assumption in the kickoff, not at demo time.
Integration feasibility — low effort, high confidence. composer require is the whole install: no config publish, no migration, no service-provider registration, no token. Auto-discovery wires it up and the only runtime dependency is Laravel's HTTP client. A first end-to-end alert into a single #ops channel is a ~15-minute exercise that exercises routing, payload building, and queueing at once. The package is small enough to read in full before committing, which is the cheapest possible due diligence.
Technical risk — concentrated in operations, not integration.
| Risk | Severity | Note |
|---|---|---|
| Webhook URL is a bearer credential | High | No second factor, no signature, no expiry. Anyone with the URL can post. Must be encrypted at rest, hidden from serialization, scrubbed from Telescope/Sentry stack traces. |
| Payload limits fail as runtime 400s | High | Oversized content/embeds/fields blow up in production, not in tests. A 400 means the notification silently never arrived — the worst failure mode for an alerting channel. |
| Rate limits are normal operation | Medium | Discord returns 429 with retry_after; the channel surfaces this as an exception. Unqueued or undeduplicated senders turn a burst into a self-inflicted outage. |
| Rotated/deleted webhook | Medium | Presents as a permanent 401/404 stream into failed_jobs where nobody looks. Needs alerting on the Discord queue plus a written rotation drill. |
| Community maintenance / bus factor | Medium | 240 stars, 0 dependents, low cadence, MIT. Mitigated by the package being three readable classes — forking or vendoring is a day, not a quarter. Laravel 13 support arrived via a laravel-shift PR, which is a healthy signal for framework-compat churn but not for feature roadmap. |
| No global config default | Low | New users expect config/discord.php; there isn't one. A null route fails at runtime — gate the channel in via() so webhook-less notifiables skip rather than throw. |
Key questions for engineering before we commit scope:
#ops webhook sufficient for the first two quarters?notifications-discord queue, and is it in failed-job alerting on day one?^1.8 minor pin with a mandatory manual smoke test on every bump, and does the PHP floor implied by Laravel 13 clear our platform?Recommendation: adopt (buy, don't build). The per-feature saving over an in-house webhook client is small, but it compounds across every roadmap item that posts to Discord, and it retires a recurring class of bespoke payload-formatting and retry bugs. The residual risks are operational hygiene we'd have to solve for a homegrown client anyway.
Stack fit. Drops into via() alongside mail, database, and other channels with no orchestration changes. Release 1.8.0 is compatibility-only (Laravel 13.x, no API changes), so it can be taken before a framework upgrade to avoid stacking two constraint changes in one window — but confirm the implied PHP floor against our platform first. One routing subtlety to encode in our conventions: the channel calls routeNotificationForDiscord(), not routeNotificationFor('discord'), and on-demand routing (Notification::route('discord', $url)) bypasses the model method entirely.
Migration path.
DiscordMessage/DiscordEmbed, add a payload contract test asserting the JSON matches the current outbound shape, ship behind that test, then delete the old code. This is a parity exercise, not a rewrite.$hidden, plus a rotation procedure. No data migration.Compatibility and pinning. Pin ^1.8. Upgrade cadence is community-driven, so treat every bump as needing one real end-to-end smoke test — the package's test suite will not catch Discord API drift for us. Watch for minor releases that are framework-compat only (take those early) versus ones that touch the payload builder (test those deliberately).
Sequencing.
#ops webhook, one DeployFailed notification, queued. Verify the payload by dumping toArray().notifications-discord queue with tries/backoff, plus failed-job alerting on that queue. This is not optional; it is the difference between a 429 blip and a silent alerting outage.Str::limit) applied to exception messages and stack snippets before they reach a field.$hidden, redaction configured in error tracking, rotation runbook written before it's needed.via() by webhook presence. Use allowedMentions(['parse' => []]) as the default posture and permit mentions deliberately on the one or two notifications that should page a human.Explicitly out of sequence until validated under load: on-call paging.
Maintenance. Our burden is our own notifications, payload tests, and secret handling — not the package. The integration surface is three classes (DiscordChannel, DiscordMessage, DiscordEmbed), readable in full and forkable in roughly a day under MIT. Budget an internal owner for the fork path rather than an SLA; there is no vendor support, only GitHub issues and PRs.
Support. Community-maintained, effectively one to two maintainers. Laravel 13 compatibility arriving via laravel-shift suggests the ecosystem keeps the framework constraint current, but nothing guarantees feature work. The realistic escalation path for a blocking bug is a local patch, so verify the package is vendorable in our deploy pipeline before we depend on it in a revenue-adjacent flow.
Scaling. Throughput is bounded by Discord's per-webhook and global rate limits, not by anything the package does — no connection pooling, no long-running process, no state to shard. The scaling model is therefore organizational and structural: a webhook per tenant/team, isolated queues so a 429 burst can't starve mail, and deduplication/collapse at the source. A monitor that fires on every failed health check will burn the limit and then fail all subsequent alerts; throttling belongs upstream of the queue, not inside it. Fan-out to N channels means N notifications — loop over webhook URLs rather than expecting one payload to multi-target.
Failure modes to instrument.
| Symptom | Cause | Detection |
|---|---|---|
| Silent non-delivery | 400 from payload limits or an empty message (no content, no embeds) |
Payload contract test in CI; assert non-empty before send |
| Retry storm / failed jobs | 429 under burst, insufficient tries/backoff or no dedupe |
Queue depth and failed-job count on notifications-discord |
| Permanent failure | Webhook rotated or deleted → 401/404 forever | Alert on the Discord queue; rotation drill |
| Runtime job failure | routeNotificationForDiscord() returns null |
Gate the channel in via() |
| Credential exposure | Webhook URI persisted by Telescope/Sentry in an exception | Redaction config; assume it will otherwise appear in a stack trace |
Ramp-up. A Laravel-fluent engineer reaches a working notification in about 15 minutes; a production-grade pattern (queue isolation, payload tests, truncation, severity map, mention discipline) in roughly half a day; multi-tenant webhook storage plus a rotation runbook in one to two days. There is no new infrastructure to learn — no bot hosting, no Discord application or permission model — so the real ramp-up cost is learning Discord's payload semantics and limits, and internalizing that this is a deliberately narrow, one-way channel. Write that scope boundary down; it prevents the most likely stakeholder disappointment later.
How can I help you explore Laravel packages today?