Notification Hub Feature Guide
Overview
The Notification Hub is a comprehensive multi-channel notification system that supports email, SMS, Slack, and in-app notifications. It provides users with granular control over their notification preferences, quiet hours, and channel configurations.
Key Features
- Multi-channel delivery: Email, SMS (Twilio), Slack webhooks, and in-app notifications
- User preferences: Per-notification-type, per-channel preferences
- Quiet hours: Configurable do-not-disturb periods with timezone support
- Digest batching: Hourly, daily, or weekly notification digests
- One-click unsubscribe: Token-based email unsubscribe links
- Template system: Customizable notification templates with variable substitution
- Admin management: Full CRUD for notification types and templates
Architecture
Models
| Model | Description |
|---|---|
NotificationChannel | Available notification channels (email, SMS, etc.) |
NotificationType | Notification types with categories (billing, project, security) |
NotificationPreference | User preferences per type/channel |
UserQuietHours | User-configurable quiet hours |
UserChannelConfig | Channel-specific configuration (phone number, webhook URL) |
NotificationQueue | Pending notifications awaiting delivery |
NotificationDelivery | Delivery tracking and history |
NotificationDigest | Batched notification digests |
NotificationTemplate | Customizable message templates |
UnsubscribeToken | Token-based unsubscribe links |
Services
- NotificationHub: Central service for sending and managing notifications
- DigestService: Processes and sends notification digests
Channel Drivers
Located in App\Services\Notifications\Channels\:
EmailChannel- Sends via Laravel MailInAppChannel- Database notificationsSlackChannel- Slack webhook integrationSmsChannel- Twilio SMS integration
Usage
Sending Notifications
use App\Services\Notifications\NotificationHub;
// Inject or resolve the hub
$hub = app(NotificationHub::class);
// Send a notification
$hub->send($user, 'invoice.created', [
'user_name' => $user->name,
'invoice_number' => 'INV-001',
'amount' => '$1,500.00',
'due_date' => 'January 15, 2024',
]);
Processing the Queue
Run via scheduler or manually:
use App\Jobs\ProcessNotificationQueueJob;
// Process pending notifications
ProcessNotificationQueueJob::dispatch();
Sending Digests
use App\Jobs\SendNotificationDigestsJob;
// Send all digest frequencies
SendNotificationDigestsJob::dispatchAll();
// Or specific frequency
SendNotificationDigestsJob::dispatch('daily');
Generating Unsubscribe Links
use App\Models\UnsubscribeToken;
// Generate for all notifications
$token = UnsubscribeToken::generateForUser($user, 'all');
// Generate for a category
$token = UnsubscribeToken::generateForUser($user, 'category:billing');
// Generate for a specific type
$token = UnsubscribeToken::generateForUser($user, 'notification_type:invoice.created');
$unsubscribeUrl = route('notifications.unsubscribe.show', $token->token);
Routes
User Routes (requires auth)
| Method | URI | Name | Description |
|---|---|---|---|
| GET | /settings/notifications | settings.notifications.index | View preferences |
| PUT | /settings/notifications/preferences | settings.notifications.preferences | Update preferences |
| PUT | /settings/notifications/quiet-hours | settings.notifications.quiet-hours | Update quiet hours |
| POST | /settings/notifications/channels/{channel}/setup | settings.notifications.channel.setup | Configure channel |
| POST | /settings/notifications/channels/{channel}/verify | settings.notifications.channel.verify | Verify channel |
| DELETE | /settings/notifications/channels/{channel} | settings.notifications.channel.remove | Remove channel |
| POST | /settings/notifications/resubscribe | settings.notifications.resubscribe | Re-enable all |
Public Routes
| Method | URI | Name | Description |
|---|---|---|---|
| GET | /unsubscribe/{token} | notifications.unsubscribe.show | View unsubscribe page |
| POST | /unsubscribe/{token} | notifications.unsubscribe.process | Process unsubscribe |
Admin Routes (requires admin)
| Method | URI | Name | Description |
|---|---|---|---|
| GET | /admin/notifications/types | admin.notifications.types.index | List types |
| POST | /admin/notifications/types | admin.notifications.types.store | Create type |
| PUT | /admin/notifications/types/{type} | admin.notifications.types.update | Update type |
| DELETE | /admin/notifications/types/{type} | admin.notifications.types.destroy | Delete type |
| GET | /admin/notifications/templates | admin.notifications.templates.index | List templates |
| POST | /admin/notifications/templates | admin.notifications.templates.store | Create template |
| PUT | /admin/notifications/templates/{template} | admin.notifications.templates.update | Update template |
| DELETE | /admin/notifications/templates/{template} | admin.notifications.templates.destroy | Delete template |
| GET | /admin/notifications/templates/{template}/preview | admin.notifications.templates.preview | Preview template |
| POST | /admin/notifications/templates/{template}/duplicate | admin.notifications.templates.duplicate | Duplicate template |
Notification Types
Default notification types are seeded via NotificationHubSeeder:
Billing
invoice.created- When a new invoice is createdinvoice.sent- When an invoice is sentinvoice.paid- When payment is receivedinvoice.overdue- When an invoice becomes overdue (high priority)
Project
project.created- When a new project is createdproject.status_changed- When project status changesproject.file_uploaded- When a file is uploaded (low priority)message.received- When a new message is received
Security (cannot unsubscribe)
security.login- New login detectedsecurity.password_changed- Password was changed (urgent)
System
system.maintenance- Scheduled maintenance notifications
Template Variables
Templates support Blade-style variable substitution:
<p>Hello {{ user_name }},</p>
<p>A new invoice #{{ invoice_number }} for {{ amount }} has been created.</p>
<p>Due date: {{ due_date }}</p>
Configuration
Environment Variables
# Twilio (for SMS)
TWILIO_SID=your_account_sid
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=+15551234567
# Slack (webhook URLs configured per-user)
Scheduler (Console Kernel)
Add to app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
// Process notification queue every minute
$schedule->job(new ProcessNotificationQueueJob)->everyMinute();
// Send hourly digests
$schedule->job(new SendNotificationDigestsJob('hourly'))->hourly();
// Send daily digests at 8am
$schedule->job(new SendNotificationDigestsJob('daily'))->dailyAt('08:00');
// Send weekly digests on Monday at 8am
$schedule->job(new SendNotificationDigestsJob('weekly'))->weeklyOn(1, '08:00');
}
Quiet Hours
Users can configure quiet hours to suppress notifications during specific times:
- Start/End Time: 24-hour format (e.g., 22:00 - 08:00)
- Days of Week: 0 (Sunday) through 6 (Saturday)
- Timezone: User's local timezone
- Allow Urgent: Optional bypass for urgent notifications
When quiet hours are active, notifications are queued and delivered after quiet hours end.
Channel Verification
Some channels (like SMS) require verification:
- User submits phone number via channel setup
- System generates 6-digit verification code
- Code is sent to the phone number
- User submits code via channel verify endpoint
- Channel is marked as verified and enabled
Security Considerations
- Security-category notifications cannot be unsubscribed (e.g., login alerts)
- Unsubscribe tokens expire after 7 days
- Channel verification codes expire after 10 minutes
- Webhook URLs are validated before saving
Testing
Run notification hub tests:
php artisan test tests/Feature/NotificationHubTest.php
Email Tracking with Resend
The notification system supports email tracking (opens, clicks, bounces) via Resend webhooks.
Setup
-
Enable tracking in Resend Dashboard:
- Go to Resend Dashboard
- Select your domain → Configuration
- Enable Open Tracking and Click Tracking
-
Create webhook in Resend:
- Go to Webhooks → Add Webhook
- URL:
https://yourdomain.com/webhooks/resend - Select events:
email.sent,email.delivered,email.opened,email.clicked,email.bounced,email.complained,email.delivery_delayed,email.failed,email.suppressed - Save and copy the signing secret
-
Configure environment:
MAIL_MAILER=resend RESEND_API_KEY=re_your_api_key RESEND_WEBHOOK_SECRET=whsec_your_signing_secret
Tracked Events
| Event | Description | Updates |
|---|---|---|
email.sent | Email queued for delivery | - |
email.delivered | Email delivered to inbox | status, delivered_at |
email.opened | Recipient opened email | opened_at, open count |
email.clicked | Recipient clicked a link | clicked_at, clicked URL |
email.bounced | Email bounced (hard/soft) | status, bounce type |
email.complained | Recipient marked as spam | status |
email.delivery_delayed | Delivery is delayed | status, delay reason |
email.failed | Delivery permanently failed | status, failure reason |
email.suppressed | Email suppressed (previous bounce) | status |
Delivery Statuses
NotificationDelivery::STATUS_SENT // 'sent'
NotificationDelivery::STATUS_DELIVERED // 'delivered'
NotificationDelivery::STATUS_BOUNCED // 'bounced'
NotificationDelivery::STATUS_FAILED // 'failed'
NotificationDelivery::STATUS_COMPLAINED // 'complained'
NotificationDelivery::STATUS_DELAYED // 'delayed'
NotificationDelivery::STATUS_SUPPRESSED // 'suppressed'
Tracking Data
The tracking_data JSON column stores additional webhook data:
// Example tracking_data for opened + clicked email
[
'opens' => 3,
'opens_last' => '2024-01-22T10:30:00Z',
'clicks' => 1,
'last_click' => '2024-01-22T10:31:00Z',
'clicked_url' => 'https://example.com/invoice/123',
'last_user_agent' => 'Mozilla/5.0...',
]
Webhook Controller
Located at App\Http\Controllers\Webhooks\ResendWebhookController:
- Verifies Svix webhook signatures
- Matches emails by
external_id(Resend email ID) - Updates delivery status and tracking data
Related Files
App\Services\Notifications\NotificationHub- Core serviceApp\Services\Notifications\DigestService- Digest processingApp\Services\Notifications\Channels\EmailChannel- Email delivery with ResendApp\Http\Controllers\Webhooks\ResendWebhookController- Webhook handlerApp\Providers\NotificationHubServiceProvider- Service registrationApp\Http\Controllers\NotificationHubController- User preferencesApp\Http\Controllers\Admin\NotificationTypeController- Admin typesApp\Http\Controllers\Admin\NotificationTemplateController- Admin templatesdatabase/seeders/NotificationHubSeeder.php- Default data