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_id because PostgreSQL doesn't support cross-database foreign keys (users live in central DB, notifications in tenant DB). The application layer handles the association via the forUser() 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.created events 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_url when 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.