Notifications
AutoCom includes a persistent, real-time notification system that stores notifications in the tenant database and delivers them instantly via WebSocket. Any module can send notifications without registration — just call the NotificationService.
How It Works
Module calls NotificationService::send()
│
├─► Row created in tenant `notifications` table
│
└─► NotificationCreated event dispatched (ShouldBroadcast)
│
▼
Horizon processes broadcast job
│
▼
Reverb delivers to private user.{id} channel
│
▼
Frontend NotificationProvider receives event
│
├─► Prepends to notification list
├─► Increments unread badge count
└─► Shows toast popup
Database Schema
Notifications are stored in the tenant database (not the central DB). The table uses UUID primary keys.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
bigint | Target user (references central users table) |
type |
string(100) | Notification type (e.g. order.created, workflow.failed) |
module |
string(50) | Source module name (e.g. orders, products, system) |
title |
string | Human-readable title |
body |
text | Detail text (optional) |
icon |
string(50) | Lucide icon name (e.g. Package, AlertTriangle) |
severity |
string(20) | info, success, warning, or error |
action_url |
string | Relative URL for click-through (optional) |
action_label |
string(100) | Button/link text (optional) |
data |
JSON | Arbitrary payload for module-specific data |
group_key |
string(100) | For batching similar notifications |
read_at |
timestamp | Null = unread |
created_at |
timestamp | |
updated_at |
timestamp |
Note: No foreign key constraint on
user_idbecause PostgreSQL doesn't support cross-database foreign keys (users live in central DB, notifications in tenant DB). The application layer handles the association via theforUser()scope.
Sending Notifications
Basic Usage
use App\Services\NotificationService;
$service = app(NotificationService::class);
$service->send($userId, [
'type' => 'order.created',
'module' => 'orders',
'title' => 'New order received',
'body' => "Order #{$order->order_number} from {$customer->name}",
'icon' => 'ShoppingCart',
'severity' => 'info',
'action_url' => "/orders/{$order->id}",
]);
Send to Multiple Users
$service->send([1, 2, 3], [
'type' => 'team.announcement',
'title' => 'Meeting in 15 minutes',
'body' => 'Daily standup in the main conference room',
'icon' => 'Users',
]);
Send to All Tenant Members
$service->sendToAllTenantMembers([
'type' => 'system.maintenance',
'module' => 'system',
'title' => 'Scheduled maintenance',
'body' => 'The system will be down for maintenance at 2:00 AM UTC',
'icon' => 'AlertTriangle',
'severity' => 'warning',
]);
Payload Reference
| Field | Required | Default | Description |
|---|---|---|---|
type |
Yes | — | Dot-notation type identifier |
title |
Yes | — | Short title shown in bell dropdown and toast |
body |
No | null | Detail text (2-line clamp in dropdown) |
module |
No | null | Source module for filtering |
icon |
No | null | Lucide icon name for the dropdown |
severity |
No | info |
info, success, warning, error — determines icon color |
action_url |
No | null | Relative path — makes the notification clickable |
action_label |
No | null | Text for the action button |
data |
No | null | Arbitrary JSON payload for programmatic use |
group_key |
No | null | Key for batching similar notifications |
Available Icons
The frontend maps these icon names to Lucide components:
Package, AlertTriangle, CheckCircle, Info, ShoppingCart, Users, Zap, Settings, MessageSquare, TrendingUp, Shield
To add more icons, update the iconMap in frontend/components/notification-dropdown.tsx.
Severity Styles
| Severity | Color | Use Case |
|---|---|---|
info |
Blue | General notifications, new orders, updates |
success |
Green | Completed workflows, milestones, confirmations |
warning |
Amber | Low stock, expiring subscriptions, degraded service |
error |
Red | Failed workflows, security alerts, out of stock |
REST API
All endpoints require authentication and tenant context (Authorization: Bearer {token}, X-Tenant: {id}). Users can only access their own notifications.
List Notifications
GET /api/v1/notifications
Query parameters:
| Param | Type | Description |
|---|---|---|
unread_only |
boolean | Filter to unread only |
type |
string | Filter by notification type |
module |
string | Filter by source module |
per_page |
integer | Results per page (1-100, default 20) |
page |
integer | Page number |
Response:
{
"notifications": [
{
"id": "a12a9a75-831a-4a73-be3b-02ca93286c90",
"user_id": 1,
"type": "order.created",
"module": "orders",
"title": "New order received",
"body": "Order #1234 from John Smith",
"icon": "ShoppingCart",
"severity": "info",
"action_url": "/orders/abc-123",
"action_label": null,
"data": null,
"group_key": null,
"read_at": null,
"created_at": "2026-02-25T18:18:40.000000Z",
"updated_at": "2026-02-25T18:18:40.000000Z"
}
],
"pagination": {
"current_page": 1,
"last_page": 1,
"per_page": 20,
"total": 1
}
}
Get Unread Count
GET /api/v1/notifications/unread-count
Response:
{ "unread_count": 5 }
Mark as Read
PATCH /api/v1/notifications/{id}/read
Mark All as Read
POST /api/v1/notifications/mark-all-read
Response:
{ "message": "All notifications marked as read", "count": 5 }
Delete Notification
DELETE /api/v1/notifications/{id}
Frontend Components
NotificationProvider
Located at frontend/contexts/notification-context.tsx. Wraps the application and provides notification state + actions.
What it does:
- Fetches the 10 most recent notifications + unread count on authentication
- Listens to
user.{id}channel for.notification.createdevents via WebSocket - Shows a toast popup when a new notification arrives
- Uses optimistic updates for mark-read and delete actions (reverts on API failure)
Context value:
interface NotificationContextValue {
notifications: AppNotification[]; // Recent notifications
unreadCount: number; // Total unread count
loading: boolean; // Initial load in progress
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
deleteNotification: (id: string) => Promise<void>;
refresh: () => Promise<void>; // Re-fetch from server
}
Usage in components:
import { useNotifications } from "@/contexts/notification-context";
function MyComponent() {
const { unreadCount, notifications, markAsRead } = useNotifications();
return (
<div>
<span>You have {unreadCount} unread notifications</span>
{notifications.map(n => (
<div key={n.id} onClick={() => markAsRead(n.id)}>
{n.title}
</div>
))}
</div>
);
}
NotificationDropdown
Located at frontend/components/notification-dropdown.tsx. The bell icon in the header with a popover dropdown.
Features:
- Bell icon with red unread count badge (shows "99+" for > 99)
- "Mark all read" button (only when unread > 0)
- Scrollable list of recent notifications with:
- Severity-colored icon
- Title (bold if unread) + unread dot indicator
- Body text (2-line clamp)
- Relative time ("just now", "5m ago", "2h ago", "3d ago")
- Click-through to
action_urlwhen set
- "View all notifications" link to
/settings/notifications
Provider Hierarchy
AuthProvider
└─ EchoProvider (WebSocket connection)
└─ NotificationProvider (listens to user channel)
└─ ThemeProvider
└─ ModulesProvider
└─ {children}
Notification Model
The App\Models\Tenant\Notification model provides useful scopes and methods:
use App\Models\Tenant\Notification;
// Query scopes
Notification::forUser($userId)->unread()->get();
Notification::forUser($userId)->ofType('order.created')->get();
Notification::forUser($userId)->fromModule('orders')->latest()->paginate(20);
// Instance methods
$notification->markAsRead();
$notification->markAsUnread();
$notification->isRead(); // returns bool
Broadcast Event
When a notification is created, a NotificationCreated event is dispatched. It extends TenantBroadcastEvent and broadcasts to the user's private channel:
- Channel:
private-user.{userId} - Event name:
.notification.created - Payload: Full notification object including
id,type,title,body,icon,severity,action_url,created_at
The event is processed by Horizon (queued via Redis) and delivered by Reverb.
Module Integration Example
Here's a complete example of a module sending notifications when an order is created:
// In modules/Orders/backend/App/Listeners/SendOrderNotification.php
namespace Modules\Orders\App\Listeners;
use App\Services\NotificationService;
use Modules\Orders\App\Events\OrderCreated;
class SendOrderNotification
{
public function handle(OrderCreated $event): void
{
$order = $event->order;
// Notify the store owner
app(NotificationService::class)->send($order->store_owner_id, [
'type' => 'order.created',
'module' => 'orders',
'title' => 'New order received',
'body' => "Order #{$order->order_number} - {$order->item_count} items, \${$order->total}",
'icon' => 'ShoppingCart',
'severity' => 'info',
'action_url' => "/orders/{$order->id}",
'data' => [
'order_id' => $order->id,
'total' => $order->total,
],
]);
}
}
No channel registration or configuration is needed. The NotificationService handles database persistence and real-time delivery automatically.