Notifications Guide
Last Updated: 2026-01-13 Status: Implemented Plan Reference: 010-notifications-email.md, 079-notifications-improvement.md
Overview
The Notifications system handles both email and in-app notifications to keep users informed about important events. It supports multiple channels (mail, database), notification preferences, and integrates with Laravel's notification system.
Table of Contents
- Accessing Notifications
- Features
- Notification Center
- Notification Digest
- Notification Templates
- Notification Types
- How to Use
- Email Configuration
- Technical Architecture
- Delivery Tracking Dashboard
- Related Features
Accessing Notifications
Navigation
| Access Point | Location | URL | Role |
|---|---|---|---|
| Notification Bell | Top navigation | Click bell icon | Both |
| All Notifications | Dropdown menu | /notifications | Both |
| Notification Preferences | Profile settings | /profile#notifications | Both |
Permissions
| Action | Admin | Client User |
|---|---|---|
| View own notifications | ✅ | ✅ |
| Mark as read | ✅ | ✅ |
| Delete notifications | ✅ | ✅ |
| Configure preferences | ✅ | ✅ |
Features
In-App Notifications
- Real-time notification bell
- Unread count badge
- Notification dropdown
- Mark as read/unread
- Delete notifications
Email Notifications
- HTML email templates
- Plain text fallback
- Customizable sender
- Queue for performance
- Retry on failure
Notification Preferences
- Per-notification-type settings
- Email on/off toggle
- In-app on/off toggle
- Frequency settings
Notification Center
The notification center (/notifications) provides comprehensive notification management with advanced filtering.
Features
- Filtering: View all, unread only, or read only notifications
- Type Filtering: Filter by notification type
- Mark as Read: Individual or bulk mark as read
- Delete: Remove individual notifications
- Clear All: Delete all notifications at once
- Pagination: 20 notifications per page
AJAX Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/notifications/recent | GET | Get recent notifications (JSON) |
/notifications/unread-count | GET | Get unread count (JSON) |
/notifications/{id}/read | POST | Mark single notification as read |
/notifications/mark-all-read | POST | Mark all as read |
/notifications/clear-all | DELETE | Delete all notifications |
JavaScript Integration
// Fetch unread count
fetch('/notifications/unread-count')
.then(res => res.json())
.then(data => updateBadge(data.count));
// Fetch recent notifications
fetch('/notifications/recent')
.then(res => res.json())
.then(data => renderNotifications(data.notifications));
// Mark as read (AJAX)
fetch(`/notifications/${id}/read`, { method: 'POST' })
.then(res => res.json())
.then(data => updateUI());
Notification Digest
The digest system sends consolidated notification summaries to users who prefer batched emails.
User Preferences
Users can enable digest mode in their profile settings:
| Preference | Column | Default |
|---|---|---|
| Enable Digest | notification_digest | false |
| Push Notifications | push_notifications_enabled | false |
| Email Notifications | email_notifications_enabled | true |
| Digest Frequency | digest_frequency | daily |
Digest Service
Location: app/Services/NotificationDigestService.php
use App\Services\NotificationDigestService;
$service = new NotificationDigestService();
// Send digests to all eligible users
$count = $service->sendDigests();
// Check if user is eligible
$eligible = $service->isEligibleForDigest($user);
// Preview digest for a specific user
$preview = $service->getDigestPreview($user);
Scheduling Digests
Add to scheduler (app/Console/Kernel.php):
$schedule->call(function () {
app(NotificationDigestService::class)->sendDigests();
})->dailyAt('08:00');
Digest Email Contents
- Groups notifications by type
- Shows count per type
- Lists individual notification messages
- Links to full notification center
Notification Templates
Templates allow customization of notification content with variable substitution.
Template Model
Location: app/Models/NotificationTemplate.php
| Column | Type | Description |
|---|---|---|
name | string | Display name |
type | string | Unique type identifier |
subject | string | Email subject with variables |
body | text | Email body with variables |
channels | json | Delivery channels |
variables | json | Available variables |
is_active | boolean | Template status |
Creating Templates
use App\Models\NotificationTemplate;
$template = NotificationTemplate::create([
'name' => 'Project Created',
'type' => 'project_created',
'subject' => 'New Project: {{ project_name }}',
'body' => 'A new project "{{ project_name }}" has been created for {{ client_name }}.',
'channels' => ['mail', 'database'],
'variables' => ['project_name', 'client_name'],
'is_active' => true,
]);
Rendering Templates
$template = NotificationTemplate::findByType('project_created');
$rendered = $template->render([
'project_name' => 'Website Redesign',
'client_name' => 'Acme Corp',
]);
// Result:
// [
// 'subject' => 'New Project: Website Redesign',
// 'body' => 'A new project "Website Redesign" has been created for Acme Corp.',
// ]
Template Scopes
// Find active template by type
$template = NotificationTemplate::findByType('invoice_created');
// Get all active templates
$active = NotificationTemplate::active()->get();
// Filter by type
$invoiceTemplates = NotificationTemplate::ofType('invoice%')->get();
Variable Placeholders
Templates use {{ variable }} syntax for placeholders:
// Get available placeholders
$placeholders = $template->getPlaceholders();
// Returns: ['project_name', 'client_name']
Notification Types
Admin Notifications
| Notification | Trigger | Channels |
|---|---|---|
| New Client Registration | Client user registers | Email, Database |
| Invoice Overdue | Invoice past due date | Email, Database |
| Project Status Change | Project status updated | Database |
| File Uploaded | Client uploads file | Email, Database |
| New Support Message | Client sends message | Email, Database |
Client Notifications
| Notification | Trigger | Channels |
|---|---|---|
| Invoice Created | New invoice created | Email, Database |
| Invoice Reminder | Approaching due date | |
| Project Update | Project status changed | Email, Database |
| File Shared | New file made visible | Email, Database |
| Welcome Email | Account created |
How to Use
Viewing Notifications
- Click the bell icon in the navigation bar
- See recent unread notifications
- Click "View All" for full list
- Click a notification to see details
Managing Notifications
Mark as Read:
- Click notification in dropdown
- Or click "Mark as Read" button
- Or click "Mark All as Read"
Delete Notification:
- Navigate to notifications page
- Click delete icon next to notification
- Confirm deletion
Configuring Preferences
- Navigate to Profile → Notification Preferences
- Toggle settings for each notification type:
- Email: Receive email notifications
- In-App: Show in notification center
- Click "Save Preferences"
Email Configuration
Environment Variables
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@yourcompany.com
MAIL_FROM_NAME="Your Company"
Supported Mail Drivers
| Driver | Use Case |
|---|---|
smtp | Standard SMTP server |
mailgun | Mailgun API |
ses | Amazon SES |
postmark | Postmark API |
log | Local development (logs only) |
Email Templates
Email templates are located in resources/views/emails/:
| Template | Purpose |
|---|---|
invoice-created.blade.php | New invoice notification |
invoice-reminder.blade.php | Payment reminder |
welcome.blade.php | Welcome email |
file-shared.blade.php | File notification |
project-update.blade.php | Project status change |
Technical Architecture
Notification Classes
Location: app/Notifications/
// Example: InvoiceCreated notification
class InvoiceCreated extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public Invoice $invoice
) {}
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('New Invoice #' . $this->invoice->invoice_number)
->line('A new invoice has been created.')
->action('View Invoice', route('portal.invoices.show', $this->invoice))
->line('Amount due: $' . number_format($this->invoice->total, 2));
}
public function toArray(object $notifiable): array
{
return [
'type' => 'invoice_created',
'invoice_id' => $this->invoice->id,
'invoice_number' => $this->invoice->invoice_number,
'amount' => $this->invoice->total,
'message' => 'New invoice #' . $this->invoice->invoice_number,
];
}
}
Sending Notifications
// Send to single user
$user->notify(new InvoiceCreated($invoice));
// Send to multiple users
Notification::send($users, new InvoiceCreated($invoice));
// Queue notification
$user->notify((new InvoiceCreated($invoice))->delay(now()->addMinutes(5)));
Available Notifications
| Class | Event |
|---|---|
InvoiceCreated | Invoice created |
InvoiceReminder | Invoice due soon |
InvoiceOverdue | Invoice past due |
ProjectStatusChanged | Project status updated |
FileShared | File made visible |
WelcomeNotification | User registered |
DocumentRequestCreated | Document requested |
DeliverableSubmitted | Deliverable ready |
Controller
Location: app/Http/Controllers/NotificationController.php
| Method | Route | Description |
|---|---|---|
index() | GET /notifications | List notifications with filtering |
recent() | GET /notifications/recent | Recent notifications (JSON) |
unreadCount() | GET /notifications/unread-count | Unread count (JSON) |
markAsRead() | POST /notifications/{id}/read | Mark one read |
markAllAsRead() | POST /notifications/mark-all-read | Mark all read |
clearAll() | DELETE /notifications/clear-all | Delete all notifications |
destroy() | DELETE /notifications/{id} | Delete one |
Routes
Route::middleware('auth')->prefix('notifications')->name('notifications.')->group(function () {
Route::get('/', [NotificationController::class, 'index'])->name('index');
Route::get('/recent', [NotificationController::class, 'recent'])->name('recent');
Route::get('/unread-count', [NotificationController::class, 'unreadCount'])->name('unread-count');
Route::post('/{id}/read', [NotificationController::class, 'markAsRead'])->name('read');
Route::post('/mark-all-read', [NotificationController::class, 'markAllAsRead'])->name('mark-all-read');
Route::delete('/clear-all', [NotificationController::class, 'clearAll'])->name('clear-all');
Route::delete('/{id}', [NotificationController::class, 'destroy'])->name('destroy');
});
Database Schema
Table: notifications (Laravel default)
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
type | string | Notification class name |
notifiable_type | string | User model |
notifiable_id | bigint | User ID |
data | json | Notification data |
read_at | timestamp | When read |
created_at | timestamp | When created |
Table: notification_preferences
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
user_id | bigint | User FK |
notification_type | string | Type identifier |
email_enabled | boolean | Email toggle |
database_enabled | boolean | In-app toggle |
Table: notification_templates
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
name | string | Template display name |
type | string | Unique type identifier |
subject | string | Email subject with variables |
body | text | Email body with variables |
channels | json | Delivery channels |
variables | json | Available variables |
is_active | boolean | Template enabled |
created_at | timestamp | Created timestamp |
updated_at | timestamp | Updated timestamp |
User Preference Columns (added to users table)
| Column | Type | Default | Description |
|---|---|---|---|
notification_digest | boolean | false | Enable digest emails |
push_notifications_enabled | boolean | false | Enable push notifications |
email_notifications_enabled | boolean | true | Enable email notifications |
digest_frequency | string | daily | Digest frequency |
User Model Methods
// Get unread notifications
$user->unreadNotifications;
// Get all notifications
$user->notifications;
// Get unread count
$user->unreadNotifications()->count();
// Mark specific notification as read
$notification->markAsRead();
// Mark all as read
$user->unreadNotifications->markAsRead();
Notification Preferences
Default Preferences
// app/Models/User.php
public function getNotificationPreference(string $type): array
{
$preference = $this->notificationPreferences()
->where('notification_type', $type)
->first();
return [
'email' => $preference?->email_enabled ?? true,
'database' => $preference?->database_enabled ?? true,
];
}
Checking Preferences
// In notification class
public function via(object $notifiable): array
{
$channels = [];
$prefs = $notifiable->getNotificationPreference('invoice_created');
if ($prefs['mail']) {
$channels[] = 'mail';
}
if ($prefs['database']) {
$channels[] = 'database';
}
return $channels;
}
Delivery Tracking Dashboard
The delivery dashboard provides real-time monitoring of notification deliveries with retry management for failed notifications.
Accessing the Dashboard
- URL: Admin → Notifications → Delivery (
/admin/notifications/delivery) - Navigation: Click "Delivery" in the notification management navigation bar
Dashboard Features
| Feature | Description |
|---|---|
| Summary Cards | Real-time counts for sent, delivered, failed, opened, clicked |
| Delivery Rate | Percentage of successfully delivered notifications |
| Open/Click Rates | Engagement metrics for delivered notifications |
| Queue Status | Pending, scheduled, processing, sent, failed counts |
| Timeline Chart | Hourly delivery breakdown over last 24 hours |
| Channel Breakdown | Delivery stats per channel (email, database, etc.) |
| Failed Notifications | List of failed notifications with retry actions |
Retry Management
The system supports automatic and manual retry of failed notifications:
use App\Services\Notifications\NotificationDeliveryService;
$service = app(NotificationDeliveryService::class);
// Retry single notification
$result = $service->retryNotification($queueId);
// Bulk retry multiple notifications
$result = $service->bulkRetry([1, 2, 3]);
// Retry all eligible failed notifications
$result = $service->retryAllFailed();
Retry Delays
Failed notifications are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 hours |
| Maximum | 5 retries total |
Dashboard API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/admin/notifications/delivery | GET | Dashboard view |
/admin/notifications/delivery/metrics | GET | JSON metrics for AJAX refresh |
/admin/notifications/delivery/failed | GET | Failed notifications list |
/admin/notifications/delivery/{id}/retry | POST | Retry single notification |
/admin/notifications/delivery/bulk-retry | POST | Retry multiple notifications |
/admin/notifications/delivery/retry-all | POST | Retry all eligible |
/admin/notifications/delivery/{id}/cancel | POST | Cancel queued notification |
/admin/notifications/delivery/detail/{uuid} | GET | Delivery details |
/admin/notifications/delivery/clear-old | POST | Clear old queue items |
RetryNotificationJob
Failed notifications are retried via a queued job:
use App\Jobs\RetryNotificationJob;
use App\Models\NotificationQueue;
// Dispatch retry job with delay
$notification = NotificationQueue::find($id);
RetryNotificationJob::dispatch($notification)
->delay(now()->addMinutes(5));
Database Tables
Table: notification_queue
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
uuid | uuid | Unique identifier |
user_id | bigint | Target user |
notification_type_id | bigint | Notification type |
channel | string | Delivery channel |
status | string | pending/scheduled/processing/sent/failed/cancelled |
priority | integer | Delivery priority |
attempt_count | integer | Number of attempts |
last_error | text | Last error message |
sent_at | timestamp | When successfully sent |
created_at | timestamp | Queue time |
Table: notification_deliveries
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
uuid | uuid | Tracking identifier |
user_id | bigint | Recipient user |
notification_type_id | bigint | Notification type |
channel | string | Delivery channel |
status | string | delivered/failed |
data | json | Notification content |
external_id | string | External service ID |
delivered_at | timestamp | Delivery timestamp |
opened_at | timestamp | When opened |
clicked_at | timestamp | When clicked |
tracking_data | json | Additional tracking |
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Authentication | Notifications require login |
| Background Jobs | Queued email sending |
Complementary Features
| Feature | Description |
|---|---|
| Webhooks | External notifications |
| Realtime | Push notifications |
| Activity Logging | Logs notification events |
Best Practices
For Administrators
- Configure email properly before going live
- Test email delivery with real addresses
- Monitor failed emails in logs
- Respect user preferences for notifications
For Developers
- Queue all email notifications for performance
- Keep notification data minimal in database
- Use meaningful notification types for preferences
- Implement ShouldQueue interface
Troubleshooting
| Issue | Solution |
|---|---|
| Emails not sending | Check MAIL_* config and queue worker |
| Notification not showing | Verify database channel in via() |
| Duplicate notifications | Check if event fires multiple times |
| Slow notification | Ensure ShouldQueue is implemented |
Testing Email Locally
# Use log driver for testing
MAIL_MAILER=log
# Check logs
tail -f storage/logs/laravel.log
Queue Issues
# Check queue
php artisan queue:work
# Check failed jobs
php artisan queue:failed
# Retry failed
php artisan queue:retry all
See Also
- Webhooks - External notifications
- Realtime - Push notifications
- Background Jobs - Queue processing