SIGNALS Documentation
API Reference

Plugin channel providers

Build a Slack/Teams-style notification channel plugin — ChannelDriver contract, channel_drivers manifest section, config schema, testing, and lifecycle expectations.

Overview

Plugins can supply notification channel providers — delivery drivers such as Slack incoming webhooks or Microsoft Teams connectors. Core already ships email, database, and broadcast. A channel-provider plugin declares drivers in signals.yaml, registers them through PluginRegistrar::channelDriver(), and stores tenant credentials on channel_providers rows (encrypted config) managed by the admin Notifications settings UI and the Channel Providers API.

Reference implementation: signals/slack-webhook under plugins/signals/slack-webhook/.

Related contracts:


Contract walkthrough

Implement App\Contracts\Notifications\ChannelDriver:

Method Role
key() Stable driver key — must match the vendor-prefixed manifest channel_drivers[].key (e.g. signals.slack_webhook)
channel() Logical delivery channel (e.g. slack) — may appear in notifications[].channels
label() Human label for the Channel Providers panel
configSchema() list<ChannelConfigFieldData> driving the admin form + secret masking
resolveConfig() Effective config at send/test time (usually the active ChannelProvider row)
send(ChannelSendData) Deliver one payload (called sync by SendChannelNotification / registry-backed Laravel channel)
test(TestChannelProviderData) Connectivity self-test for the admin Send test button and API

Register in PluginBase::register():

$registrar->channelDriver('signals.slack_webhook', SlackWebhookChannelDriver::class);

Wiring ⊆ declaration: when the registrar is scoped via forPlugin(), the key must appear in channel_drivers[] or registration throws PluginRegistrarException.


Manifest channel_drivers

channel_drivers:
  - key: signals.slack_webhook
    channel: slack
    label: Slack Incoming Webhook
    config_fields:
      - webhook_url
      - default_channel

notifications:
  - key: signals.slack_webhook.test_ping
    label: Slack Test Ping
    channels: [slack]

Rules (see Manifest reference):

  • Keys are vendor-prefixed like permissions ({vendor}.…).
  • channel is a lowercase slug and must not collide with core database / mail / broadcast.
  • notifications[].channels may list core channels or logical channels declared in the same plugin's channel_drivers[].
  • config_fields is optional documentation for review; the runtime form comes from configSchema().

Declare outbound hosts under network when the driver calls external HTTP (e.g. hooks.slack.com).


Config schema & admin UI

Tenant configuration lives on ChannelProvider rows (driver, encrypted config, is_active, last test status) — not plugin settings — so operators manage Slack/Teams credentials next to Email/Database/Broadcast under Admin → Settings → Notifications → Channel Providers.

  • Secret fields (secret: true) are masked on read and preserved on leave-blank save.
  • Plugin-supplied drivers show a Plugin badge.
  • When the plugin is disabled or uninstalled, existing rows become inert and display Provider unavailable (configure/test are blocked with a toast).

Permissions (no extra plugin permission required for core provider CRUD):

Surface Gate
Admin Notifications UI notifications.manage
API read channel-providers:read (+ notifications.manage)
API write / test channel-providers:write (+ notifications.manage)

Add a plugin-declared permission only if you expose a custom page or slot that needs its own grant.


Delivery & preferences

  1. Driver registration happens at plugin boot (PluginServiceProvider loads enabled packages only).
  2. Non-core logical channels are bridged into Laravel's notification channel manager so Signals::notify() / SendNotification can deliver them.
  3. NotificationChannelResolver silently drops channels whose drivers are not registered (intersect-only model).
  4. Account preference catalogues pick up plugin channels dynamically via the registry label map.
  5. Queued mid-flight rule: if a channel becomes unavailable after the job was queued, skip that channel with a warning log and still deliver remaining channels — never fail the whole job.

Lifecycle expectations

Event Behaviour
Enable Next boot registers the driver; providers become configurable/testable again
Disable Driver is not registered; ChannelProvider rows stay; UI flags Provider unavailable; delivery skips with a warning
Uninstall Keep rows inert (same as disable). Prompted cleanup of orphaned provider rows is a future admin UX pass — do not delete tenant config automatically

Testing

Unit/feature pattern for a channel plugin:

  1. Parse/validate the package signals.yaml (including channel_drivers + lifted notification channels).
  2. Register via PluginRegistrar::forPlugin($manifest, $package)->channelDriver(…).
  3. Http::fake() the webhook host; call ChannelDriver::send() / ChannelProviderRegistry::test().
  4. Assert resolver inclusion when registered and silent drop when not.
  5. Assert admin panel shows the driver (plugin badge) and inert rows after the driver disappears.

See tests/Feature/Plugins/SlackWebhookPluginTest.php and tests/Feature/Services/Notifications/PluginChannelLifecycleTest.php.


Minimal checklist

  1. Add channel_drivers[] (+ network hosts) to signals.yaml.
  2. Implement ChannelDriver with vendor-prefixed key() matching the manifest.
  3. Call $registrar->channelDriver($key, $class) in register().
  4. Optionally declare a sample notifications[] entry that lists your logical channel.
  5. Cover send/test with mocked HTTP; verify disable → no crash on send.