padosoft / laravel-rebel-channel-discord
Discord delivery channel for Laravel Rebel Channels: ship security/SOC alerts (anomaly cases, lockouts, high-risk events) and notifications to a Discord channel via webhook. Part of padosoft/laravel-rebel-*.
Package info
github.com/padosoft/laravel-rebel-channel-discord
pkg:composer/padosoft/laravel-rebel-channel-discord
Requires
- php: ^8.3
- illuminate/contracts: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- padosoft/laravel-rebel-channels: ^0.1.2
- padosoft/laravel-rebel-core: ^0.1
- spatie/laravel-package-tools: ^1.92
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
This package is auto-updated.
Last update: 2026-06-04 01:30:17 UTC
README
Ship your security alerts to Discord, the Rebel way. This package plugs a Discord incoming webhook into
laravel-rebel-channelsas aMessageDeliveryChannelβ so your SOC/security alerts (anomaly cases, account lockouts, high-risk logins) and notifications land in a Discord channel, with Rebel's HMAC'd audit trail on top. Part of thepadosoft/laravel-rebel-*suite.
Table of contents
- What it is
- Quick glossary
- Why this package
- Rebel + Discord vs the alternatives
- How it works (step by step)
- Create a Discord webhook (step by step)
- Installation
- Configuration
- Usage
- Telemetry & the audit trail
- Live tests against the real webhook
.env.example- Security notes
- π Vibe coding with batteries included
- Testing & License
What it is
A thin, well-tested Discord delivery channel for Rebel Channels. It implements the Channels
MessageDeliveryChannel contract (key='discord', supports(Channel::Discord)) and posts a
message to a Discord channel through an incoming webhook β no bot, no OAuth, just a URL.
Its first job is delivering security/SOC alerts: when Rebel detects an anomaly case, an account
lockout, or a high-risk event, you forward a human-readable line to your #soc-alerts channel so
the team sees it in real time. It also works for plain notifications or OTP delivery where Discord
is an acceptable transport.
A small gateway seam (DiscordGateway) wraps the HTTP call, so the whole thing is unit-testable
offline and has a real live test-suite for an actual webhook.
Depends on padosoft/laravel-rebel-core
and padosoft/laravel-rebel-channels.
Quick glossary
| Term | In plain words |
|---|---|
| Incoming webhook | A per-channel URL Discord gives you. POST JSON to it and a message appears in that channel. No bot or login needed. |
| Webhook URL | https://discord.com/api/webhooks/<id>/<token> β a secret: anyone with it can post to your channel. |
| SOC alert | A "Security Operations Center" notification β e.g. "account #42 locked after 5 failed logins". |
| Delivery channel | A Rebel object that knows how to send a message over one transport (Discord, SMS, β¦). Here: Discord. |
| Audit event | A row Rebel writes to rebel_auth_events so the admin panel can show what happened (who, when, which channel). |
| Keyed HMAC | A one-way, peppered hash. We store the recipient as an HMAC, never in clear, so the audit trail leaks no PII. |
Why this package
| β | What | In short |
|---|---|---|
| β β β | SOC alerts in Discord | Forward anomaly cases / lockouts / high-risk events to a channel your team already watches. |
| β β β | Zero-bot setup | Just an incoming-webhook URL β no Discord bot, no OAuth, no gateway connection to maintain. |
| β β β | Full Rebel telemetry | Every send writes a channel.delivery.sent / .failed audit event, so the panel's Channel Performance shows Discord traffic. |
| β β | Never throws out | Any transport/HTTP error becomes a clean provider_error DeliveryResult β your alerting path never explodes. |
| β β | Offline-testable | A gateway seam + fake means your tests don't hit Discord; a separate live suite does. |
| β β | Safe by default | No webhook URL β nothing registers; the URL is treated as a secret and never logged. |
| β | Privacy-first audit | The recipient (channel id) is stored only as a keyed HMAC. |
Rebel + Discord vs the alternatives
Getting a security alert into Discord, four ways:
| Capability | Rebel + this package | Shopify | Discord bot (discord.php / Gateway) | Raw Http::post() to a webhook |
|---|---|---|---|---|
| Self-hosted Discord SOC-alert channel | β | β | β (you build it) | β (you build it) |
| No bot / no OAuth (just a webhook URL) | β | β | β | β |
| Auto-wired into Rebel's delivery layer | β | β | β | β |
Graceful failure β clean provider_error |
β | β | β | β (you handle exceptions) |
| Unified audit trail (recipient HMAC'd) | β | β | β | β |
| Panel Channel Performance for Discord | β | β | β | β |
| Webhook URL kept secret / never logged | β | β | β | β (easy to leak in logs) |
| Offline test seam + live suite | β | β | β | β |
Legend: β built-in Β· β partial / DIY Β· β not available. Shopify is a closed, hosted commerce platform: it has no self-hosted Discord SOC-alert channel at all β you can't point its auth/security events at your own Discord, self-host the sender, or get a unified, HMAC'd audit trail of deliveries. A black box, not a developer-facing security-alerting channel. A Discord bot can post too, but it means running a Gateway connection and OAuth you don't need for one-way alerts. A raw
Http::post()works until you want graceful failure, a secret-safe URL, audit telemetry and tests β which is exactly what this package gives you for free.
How it works (step by step)
Rebel detects something this package Discord
(anomaly / lockout / high-risk)
β
β $channel->send($recipient, "π¨ account #42 locked", Channel::Discord, $ctx)
βΌ
ββββββββββββββββββββββββββββ HMAC(recipient) βββββββββββββββββββββββββ
β DiscordDeliveryChannel ββββββββββββββββββββββΆβ AuditLogger (core) β channel.delivery.sent
β β’ supports(Discord) β β β rebel_auth_events β / channel.delivery.failed
β β’ catches \Throwable β βββββββββββββββββββββββββ
βββββββββββββ¬βββββββββββββββ
β DiscordGateway::send($webhookUrl, {content})
βΌ
ββββββββββββββββββββββββββββ POST {"content": β¦} βββββββββββββββββββββ
β HttpDiscordGateway ββββββββββββββββββββββββββΆβ Discord webhook β βββΆ message appears
β (Illuminate Http client) β 204 No Content β #soc-alerts β in your channel
ββββββββββββββββββββββββββββ βββββββββββββββββββββ
- Something in Rebel wants to alert you. It calls the Discord delivery channel's
send(). - The channel HMACs the recipient (the Discord channel id) and asks the gateway to POST the message.
- The gateway POSTs
{"content": "<message>"}(plus optional username/avatar) to your webhook URL. Discord replies204 No Contenton success. - The channel records a
channel.delivery.sentaudit event (orchannel.delivery.failedif the POST threw) β so the admin panel's Channel Performance shows the delivery. It returns aDeliveryResulteither way; it never throws out.
Create a Discord webhook (step by step)
- In Discord, open the server and the channel you want alerts in (e.g.
#soc-alerts). (You need "Manage Webhooks" permission on that channel.) - Click the gear icon (Edit Channel) next to the channel name.
- Go to Integrations β Webhooks β New Webhook.
- Give it a name (e.g. "Rebel SOC") and optionally an avatar; make sure the target channel is selected.
- Click Copy Webhook URL. It looks like
https://discord.com/api/webhooks/123456789012345678/AbCdβ¦token. - Put it in your
.envasDISCORD_WEBHOOK_URL(see below). Done β the channel auto-registers.
Treat the URL as a secret. Anyone who has it can post to your channel. Keep it out of version control (it's in
.gitignorevia.env) and never log it. To rotate it, delete the webhook in Discord and create a new one.
Installation
composer require padosoft/laravel-rebel-channel-discord
php artisan vendor:publish --tag="rebel-channel-discord-config"
Add your webhook URL to .env:
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/123456789012345678/your-webhook-token
That's it β the delivery channel registers itself into the container under the key discord and is
tagged rebel.delivery-channels.
Configuration
File config/rebel-channel-discord.php:
| Key | Default | What it does |
|---|---|---|
webhook_url |
env(DISCORD_WEBHOOK_URL) |
The Discord incoming-webhook URL (a secret). The channel registers only when this is set. |
username |
env(DISCORD_WEBHOOK_USERNAME) |
Optional per-message display-name override. null = use the webhook's default. |
avatar_url |
env(DISCORD_WEBHOOK_AVATAR_URL) |
Optional per-message avatar override. null = use the webhook's default. |
register_provider |
true (env(REBEL_DISCORD_REGISTER)) |
Auto-register the channel into the container (only when webhook_url is set). false = installed but dormant. |
Usage
The channel implements the Channels MessageDeliveryChannel contract. Resolve it (it's bound to the
contract and tagged rebel.delivery-channels) and call send():
use Padosoft\Rebel\Channels\Contracts\MessageDeliveryChannel; use Padosoft\Rebel\Channels\Enums\Channel; use Padosoft\Rebel\Core\Context\SecurityContext; use Padosoft\Rebel\Core\Identifiers\PhoneIdentifier; $discord = app(MessageDeliveryChannel::class); // the Discord channel (when it's the registered one) // "recipient" identifies the Discord channel; its normalized value is HMAC'd for the audit trail. $recipient = PhoneIdentifier::from('+10000000042'); // e.g. a numeric channel id you assign $result = $discord->send( $recipient, 'π¨ SOC: account #42 locked after 5 failed logins (IP risk: high)', Channel::Discord, SecurityContext::fromRequest($request), ); if ($result->accepted()) { // delivered to Discord } else { // $result->reason === 'provider_error' β log/alert via another path; we never threw }
Collect every delivery channel via the tag (when you run several β Discord, Telegram, β¦):
use Padosoft\Rebel\Channel\Discord\RebelDiscordServiceProvider; use Padosoft\Rebel\Channels\Enums\Channel; /** @var iterable<\Padosoft\Rebel\Channels\Contracts\MessageDeliveryChannel> $channels */ $channels = app()->tagged(RebelDiscordServiceProvider::DELIVERY_TAG); foreach ($channels as $channel) { if ($channel->supports(Channel::Discord)) { $channel->send($recipient, $message, Channel::Discord, $context); } }
Recipient note. This channel reuses the core
PhoneIdentifieras the recipient value object (the suite's delivery contract is typed on it). Itsnormalized()value is treated as the Discord channel/webhook id and is only ever stored as a keyed HMAC. The actual POST target is always the configuredwebhook_urlβ so one configured channel maps to one Discord channel.
Telemetry & the audit trail
Delivery is only useful if you can see it. On every send this channel records one Rebel audit
event through the core AuditLogger (persisted to rebel_auth_events; sync or queued per your core
config), so the admin panel's Channel Performance shows Discord traffic β successes and
failures alike.
| Outcome | Audit event_type |
channel |
provider |
|---|---|---|---|
| Webhook accepted the post | channel.delivery.sent |
discord |
discord |
| Gateway threw (any error) | channel.delivery.failed |
discord |
discord |
Each event stores the recipient as a keyed HMAC (identifierHmac + keyVersion, never in
clear) and a metadata object:
{
"message_status": "sent",
"error_code": null
}
On failure, message_status is "failed" and error_code is "provider_error". The webhook URL
and the message content are never put in the audit metadata.
Live tests against the real webhook
The offline suite uses a fake gateway. To exercise a real Discord webhook (tests/Live), opt in
explicitly β it posts a real message to your channel:
# .env (or shell env)
REBEL_DISCORD_LIVE=1
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/123.../your-token
vendor/bin/pest --group=live
Without REBEL_DISCORD_LIVE=1 or with no webhook URL, the live test self-skips, so
composer test and external PRs never trigger a post. In CI, supply the URL as a secret and set
REBEL_DISCORD_LIVE=1 on a dedicated job.
.env.example
# The Discord incoming-webhook URL (a SECRET β never commit or log it). DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/123456789012345678/your-webhook-token # Optional per-message display overrides (empty = use the webhook defaults). DISCORD_WEBHOOK_USERNAME=Rebel SOC DISCORD_WEBHOOK_AVATAR_URL= # Auto-register the Discord delivery channel (needs DISCORD_WEBHOOK_URL). REBEL_DISCORD_REGISTER=true # Live tests (opt-in: POSTS A REAL MESSAGE) REBEL_DISCORD_LIVE=0
Security notes
- Webhook URL is a secret: anyone with it can post to your channel. It's read from
.env, never committed, and never logged β not even in exceptions (the gateway error reports only the HTTP status code). - No exception leakage: transport/HTTP errors are caught and returned as a generic
provider_errorDeliveryResultβ your alerting path never throws out. - No PII in the audit trail: the recipient (Discord channel id) is stored only as a keyed HMAC; the message content is not written to audit metadata.
- Dormant until configured: with no
DISCORD_WEBHOOK_URL(orregister_provider=false) the package installs cleanly and wires nothing.
π Vibe coding with batteries included
This package ships AI batteries β so you (and your AI agent) can extend it correctly on the first try:
CLAUDE.mdβ a concise AI working guide (purpose, conventions, architecture, how to extend, Definition of Done). Plain Markdown, so Claude Code, Cursor, Copilot and Codex all read it.AGENTS.mdβ the agent/workflow contract (branch β PR β CI β tag/release, the gates)..claude/skills/β invocable skills (at leastrebel-package-dev) encoding the suite's TDD loop, the PHPStan-level-max recipes, the security/telemetry rules, and the release discipline.
Open the repo in your AI editor and just start β the rules, guardrails and extension recipes come
with it. PRs that follow the shipped CLAUDE.md pass CI (PHPStan max + Pest + Pint) and review the
first time around.
Testing & License
composer test # Pest (delivery channel + gateway + registration; live suite self-skips) composer phpstan # static analysis, level max composer pint # code style
License: MIT β see LICENSE. Part of the padosoft/laravel-rebel suite.
