---
title: "Notifications API"
description: "Complete API documentation for Ring Platform's notification system with real-time delivery, FCM push notifications, and comprehensive management"
locale: "en"
---
# Notifications API

Ring Platform provides a comprehensive notification system with **9 REST endpoints** and real-time Tunnel delivery (native WSS, SSE, or poll). The system supports FCM push notifications, user preferences, bulk operations, and detailed analytics.

Unread counts and in-app alerts use **Tunnel Protocol** — server `publishToUserTunnel` on channel `notifications:unread`, client `useSync` via `TunnelProvider`. Closed-tab push is **FCM Admin and RFC web-push** dual-stack (see [Push notifications (FCM)](/docs/features/push-notifications-fcm.md)): Chrome → `fcm_tokens`; Safari / empty PushManager → `push_subscriptions`. Empty RFC on Chrome is a no-op, not a second delivery.

## 🏗️ System Architecture

### Notification Flow

```
Event Trigger → Notification Service → Real-time Delivery → User Preferences → FCM Push → Analytics
```

### Storage Strategy

- **Primary**: PostgreSQL with optimized queries
- **Real-time**: TunnelHub WSS / SSE / poll via `publishToUserTunnel`
- **Push**: Firebase Cloud Messaging (FCM) integration

## 📋 API Endpoints Reference

### `GET /api/notifications`

List user notifications with advanced filtering and pagination.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `page` | number | No | Page number (default: 1) |
| `limit` | number | No | Items per page (default: 20, max: 100) |
| `status` | string | No | Filter by status: `unread`, `read`, `archived` |
| `type` | string | No | Filter by type: `OPPORTUNITY`, `MESSAGE`, `ENTITY`, `SYSTEM`, `WALLET` |
| `priority` | string | No | Filter by priority: `low`, `normal`, `high`, `urgent` |
| `before` | string | No | ISO date string - show notifications before this date |
| `after` | string | No | ISO date string - show notifications after this date |

#### Example Request

{`-H "Cookie: authjs.session-token=YOUR_SESSION_COOKIE"`}

#### Response

{`"notifications": [
    {
      "id": "notif_123456",
      "userId": "user_789",
      "type": "OPPORTUNITY",
      "title": "New Partnership Opportunity",
      "message": "TechCorp is looking for AI development partners",
      "data": {
        "opportunityId": "opp_456",
        "entityId": "ent_123",
        "actionUrl": "/opportunities/opp_456"
      },
      "priority": "normal",
      "status": "unread",
      "createdAt": "2025-10-16T10:30:00Z",
      "readAt": null,
      "expiresAt": "2025-11-16T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 47,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  },
  "unreadCount": 23
}`}

### `POST /api/notifications`

Create a new notification (admin/system use primarily).

#### Request Body

{`"userId": "user_789",
  "type": "SYSTEM",
  "title": "Platform Maintenance",
  "message": "Scheduled maintenance in 2 hours",
  "priority": "high",
  "data": {
    "maintenanceStart": "2025-10-16T14:00:00Z",
    "expectedDuration": "30 minutes"
  },
  "expiresAt": "2025-10-16T16:00:00Z"
}`}

#### Response

{`"notification": {
    "id": "notif_123457",
    "userId": "user_789",
    "type": "SYSTEM",
    "title": "Platform Maintenance",
    "message": "Scheduled maintenance in 2 hours",
    "priority": "high",
    "status": "unread",
    "createdAt": "2025-10-16T12:00:00Z",
    "expiresAt": "2025-10-16T16:00:00Z"
  },
  "delivered": true,
  "pushSent": true
}`}

### `PUT /api/notifications/{id}/read`

Mark a specific notification as read.

#### Example Request

{`-H "Cookie: authjs.session-token=YOUR_SESSION_COOKIE"`}

#### Response

{`"notification": {
    "id": "notif_123456",
    "status": "read",
    "readAt": "2025-10-16T12:30:00Z"
  },
  "unreadCount": 22
}`}

### `PUT /api/notifications/read-all`

Mark all user notifications as read.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Only mark specific type as read |
| `before` | string | No | Only mark notifications before this date |

#### Example Request

{`-H "Cookie: authjs.session-token=YOUR_SESSION_COOKIE"`}

#### Response

{`"markedCount": 15,
  "unreadCount": 8
}`}

### `DELETE /api/notifications/{id}`

Delete a specific notification.

#### Example Request

{`-H "Cookie: authjs.session-token=YOUR_SESSION_COOKIE"`}

#### Response

{`"deleted": true,
  "notificationId": "notif_123456"
}`}

### `GET /api/notifications/preferences`

Get current user's notification preferences.

#### Response

{`"preferences": {
    "emailNotifications": true,
    "pushNotifications": true,
    "smsNotifications": false,
    "types": {
      "OPPORTUNITY": {
        "enabled": true,
        "email": true,
        "push": true,
        "sms": false
      },
      "MESSAGE": {
        "enabled": true,
        "email": false,
        "push": true,
        "sms": false
      },
      "ENTITY": {
        "enabled": true,
        "email": true,
        "push": false,
        "sms": false
      },
      "SYSTEM": {
        "enabled": true,
        "email": true,
        "push": true,
        "sms": false
      },
      "WALLET": {
        "enabled": true,
        "email": true,
        "push": true,
        "sms": true
      }
    },
    "quietHours": {
      "enabled": false,
      "start": "22:00",
      "end": "08:00",
      "timezone": "UTC"
    },
    "frequency": "immediate"
  }
}`}

### `PUT /api/notifications/preferences`

Update user notification preferences.

#### Request Body

{`"emailNotifications": true,
  "pushNotifications": false,
  "types": {
    "OPPORTUNITY": {
      "enabled": true,
      "email": true,
      "push": false
    },
    "MESSAGE": {
      "enabled": true,
      "email": false,
      "push": true
    }
  },
  "quietHours": {
    "enabled": true,
    "start": "23:00",
    "end": "07:00"
  }
}`}

#### Response

{`"updated": true,
  "preferences": { /* updated preferences */ }
}`}

### `POST /api/notifications/fcm/register`

Register FCM token for push notifications. **React apps should prefer the `upsertFcmToken` Server Action** (`app/_actions/fcm.ts`) — this route is rate-limited for non-React clients.

#### Request Body

{`{
  "token": "fcm_token_from_firebase_sdk",
  "deviceFingerprint": "stable-uuid-per-device",
  "deviceInfo": {
    "platform": "web",
    "userAgent": "Mozilla/5.0..."
  },
  "platform": "web"
}`}

The authenticated user id comes from `auth()` — never send `userId` in the body. One row per `(user_id, device_fingerprint)`; token rotation updates the same row.

#### Response

{`{ "success": true }`}

### `POST /api/notifications/bulk`

Send bulk notifications to multiple users (admin only).

#### Request Body

{`"userIds": ["user_123", "user_456", "user_789"],
  "notification": {
    "type": "SYSTEM",
    "title": "Important Update",
    "message": "New features available",
    "priority": "normal",
    "data": {
      "actionUrl": "/updates"
    }
  },
  "sendPush": true,
  "sendEmail": false
}`}

#### Response

{`"sent": true,
  "totalUsers": 3,
  "successful": 3,
  "failed": 0,
  "notificationIds": ["notif_123", "notif_124", "notif_125"]
}`}

### `GET /api/notifications/analytics`

Get notification analytics (admin only).

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `period` | string | No | Time period: `day`, `week`, `month` (default: `week`) |
| `startDate` | string | No | ISO date string for custom range |
| `endDate` | string | No | ISO date string for custom range |

#### Response

{`"period": "week",
  "totalSent": 1250,
  "totalRead": 980,
  "readRate": 78.4,
  "byType": {
    "OPPORTUNITY": { "sent": 450, "read": 380, "rate": 84.4 },
    "MESSAGE": { "sent": 320, "read": 280, "rate": 87.5 },
    "SYSTEM": { "sent": 180, "read": 120, "rate": 66.7 },
    "ENTITY": { "sent": 150, "read": 110, "rate": 73.3 },
    "WALLET": { "sent": 150, "read": 90, "rate": 60.0 }
  },
  "byPriority": {
    "urgent": { "sent": 50, "read": 48, "rate": 96.0 },
    "high": { "sent": 200, "read": 175, "rate": 87.5 },
    "normal": { "sent": 800, "read": 620, "rate": 77.5 },
    "low": { "sent": 200, "read": 137, "rate": 68.5 }
  },
  "deliveryStats": {
    "realTimeDelivered": 1200,
    "pushDelivered": 1100,
    "emailDelivered": 950
  }
}`}

## Real-time notifications

### Server

`features/notifications/services/notification-service.ts` calls `publishToUserTunnel` after creating a notification:

```typescript
await publishToUserTunnel(userId, 'notifications:unread', { count })
```

### Client

`NotificationProvider` uses `useUnreadCount` → `useSync` on channel `notifications:unread`. Nav badges should call `useNotificationContext()` — not duplicate `useUnreadCount()` in layout widgets.

See [Tunnel protocol](/en/docs/features/tunnel-protocol) for routes and configuration.

## Push notifications

### FCM Integration

Ring Platform integrates with Firebase Cloud Messaging. Browser registration SSOT: `hooks/use-fcm.ts` inside `FCMProvider`. The hook persists tokens via `POST /api/notifications/fcm/register` (preferred over the `upsertFcmToken` Server Action to avoid RSC revalidation storms). Logout SSOT: `useAuth().signOut()` unregisters the device token. Server send uses the **Firebase Admin SDK** (`features/notifications/services/fcm-service.ts`, `getAdminMessaging()`), not a legacy server key.

  Browser `getToken()` uses `getFcmVapidKey()` (`NEXT_PUBLIC_FIREBASE_VAPID_KEY` — Firebase Console → Cloud Messaging → Web Push certificates for this clone’s Firebase project). Dedicated `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` are runtime secrets for shipped `web-push` dual-stack — never passed to `getToken`. Full map: [Push notifications (FCM)](/docs/features/push-notifications-fcm.md).

  React shell uses `fetch('/api/notifications/fcm/register')` from `use-fcm`. The Server Action `upsertFcmToken` remains for callers that need it; both write through `lib/notifications/fcm-token-db.ts`.

#### Client setup (SSOT)

{`import { useFCM } from '@/hooks/use-fcm'
import { useAuth } from '@/hooks/use-auth'

// FCMProvider in app-client-shell wraps useFCM — handles permission,
// getToken, upsertFcmToken Server Action, and onMessage foreground toasts.

const { requestPermission, token } = useFCM()
const { signOut } = useAuth()

await signOut() // unregisters FCM for this device before session ends`}

#### Service Worker

`public/firebase-messaging-sw.js` — Firebase **12.x** compat for background messages. **Do not** add a second `push` listener while the FCM SDK is initialized (duplicate OS banners). `CALL_INVITE` / `GAME_REQUEST` are **data-only** FCM so `onBackgroundMessage` can set `requireInteraction` without a second auto-display.

RFC-only browsers register `public/push-sw.js` instead (never both on scope `/`).

See [Push notifications (FCM)](/docs/features/push-notifications-fcm.md) for the full lifecycle, migrations `016_fcm_jsonb_schema.sql` + `046_push_subscriptions_jsonb.sql`, and troubleshooting.

## 🎯 Notification Types

### OPPORTUNITY

Triggered when:
- New opportunities match user preferences
- Opportunity applications are received
- Opportunity deadlines are approaching

{`"type": "OPPORTUNITY",
  "title": "New Matching Opportunity",
  "message": "AI-powered match: Senior React Developer needed",
  "data": {
    "opportunityId": "opp_123",
    "matchScore": 95,
    "actionUrl": "/opportunities/opp_123"
  }
}`}

### MESSAGE

Triggered when:
- New direct messages received
- Group conversation updates
- Mention notifications

{`"type": "MESSAGE",
  "title": "New Message",
  "message": "John Doe: Let's discuss the project proposal",
  "data": {
    "conversationId": "conv_456",
    "senderId": "user_789",
    "actionUrl": "/messages/conv_456"
  }
}`}

### ENTITY

Triggered when:
- Entity invitations received
- Entity updates and announcements
- Verification status changes

{`"type": "ENTITY",
  "title": "Entity Invitation",
  "message": "TechCorp invited you to join their network",
  "data": {
    "entityId": "ent_123",
    "invitationId": "inv_456",
    "actionUrl": "/entities/ent_123"
  }
}`}

### SYSTEM

Triggered for:
- Platform announcements
- Maintenance notifications
- Security alerts
- Feature updates

{`"type": "SYSTEM",
  "title": "New Feature Available",
  "message": "AI-powered opportunity matching is now live!",
  "data": {
    "feature": "ai-matching",
    "actionUrl": "/features/ai-matching"
  }
}`}

### WALLET

Triggered for:
- Transaction confirmations
- Balance updates
- Staking rewards
- Payment receipts

{`"type": "WALLET",
  "title": "Payment Received",
  "message": "Received 500 RING tokens from completed project",
  "data": {
    "transactionId": "tx_123",
    "amount": "500",
    "currency": "RING",
    "actionUrl": "/wallet/transactions/tx_123"
  }
}`}

## 🔧 Implementation Examples

### React Hook for Notifications

// hooks/useNotifications.ts

{`import { useSync } from '@/hooks/use-sync'

interface Notification {
  id: string
  type: string
  title: string
  message: string
  status: 'unread' | 'read'
  createdAt: string
  data?: Record<string, any>
}

export function useNotifications() {
  const list = useSync<{ notifications: Notification[]; unreadCount: number }>({
    fetcher: async () => {
      const response = await fetch('/api/notifications')
      return response.json()
    },
    tunnel: {
      channel: 'notifications:unread',
      enabled: true,
      onMessage: () => ({ shouldRefetch: true })
    }
  })

  const markAsRead = async (notificationId: string) => {
    await fetch(\`/api/notifications/\${notificationId}/read\`, { method: 'PUT' })
    await list.refresh()
  }

  return {
    notifications: list.data?.notifications ?? [],
    unreadCount: list.data?.unreadCount ?? 0,
    markAsRead,
    refresh: list.refresh,
    tunnelConnected: list.tunnelConnected
  }
}`}

### Notification Component

// components/NotificationCenter.tsx

{`import { useNotifications } from '@/hooks/useNotifications'
import { Bell, Check } from 'lucide-react'

export function NotificationCenter() {
  const { notifications, unreadCount, markAsRead } = useNotifications()

  return (
    
      
        
        Notifications
        {unreadCount > 0 && (
          
            {unreadCount}
          
        )}
      

      
        {notifications.map(notification => (
          
            
              
                {notification.title}
                
                  {notification.message}
                
                
                  {new Date(notification.createdAt).toLocaleString()}
                
              
              {notification.status === 'unread' && (
                 markAsRead(notification.id)}
                  className="ml-2 p-1 hover:bg-gray-100 rounded"
                >
                  
                
              )}
            
          
        ))}
      
    
  )
}`}

## 🚨 Error Handling

### Common Error Responses

// Authentication Error

{`{
  "error": "Unauthorized",
  "message": "Authentication required",
  "statusCode": 401
}

// Validation Error
{
  "error": "ValidationError",
  "message": "Invalid notification data",
  "details": {
    "type": "Invalid notification type",
    "userId": "User ID is required"
  },
  "statusCode": 400
}

// Rate Limit Error
{
  "error": "RateLimitError",
  "message": "Too many requests",
  "retryAfter": 60,
  "statusCode": 429
}

// Server Error
{
  "error": "InternalServerError",
  "message": "Failed to send notification",
  "requestId": "req_123456",
  "statusCode": 500
}`}

## 🔒 Security Considerations

### Authentication
- All endpoints require valid session authentication
- Admin endpoints require `ADMIN` role
- FCM tokens are encrypted and stored securely

### Rate Limiting
- Standard users: 100 requests/minute
- Admin users: 500 requests/minute
- Bulk operations: 10 operations/minute

### Data Validation
- All input data is validated using Zod schemas
- HTML content is sanitized to prevent XSS
- File uploads are scanned for malware

### Privacy
- Notifications respect user preferences
- No sensitive data in push notification payloads
- GDPR-compliant data handling

## 📊 Monitoring & Analytics

### Real-time Metrics

// Track notification events

{`import { trackEvent } from '@/lib/analytics'
import { useTunnel } from '@/hooks/use-tunnel'

function NotificationAnalytics() {
  const { subscribe } = useTunnel()

  useEffect(() => {
    return subscribe('notifications:unread', (message) => {
      trackEvent('notification_received', {
        type: message.payload?.type,
        priority: message.payload?.priority,
        deliveryMethod: 'tunnel'
      })
    })
  }, [subscribe])
}`}

### Performance Monitoring

// Monitor delivery times

{`const startTime = Date.now()

// After publishToUserTunnel on server, client receives via useTunnel
trackEvent('notification_delivered', {
  deliveryTime: Date.now() - startTime,
  method: 'tunnel'
})`}

## 🎛️ Configuration

### Environment Variables

**Shipped FCM** — use Firebase client + Admin env from `env.local.template` (not legacy server-key names):

{`# Client — Web Push certificate for getToken() (Firebase Console → Cloud Messaging)
NEXT_PUBLIC_FIREBASE_API_KEY=…
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=…
NEXT_PUBLIC_FIREBASE_PROJECT_ID=…
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=…
NEXT_PUBLIC_FIREBASE_APP_ID=…
NEXT_PUBLIC_FIREBASE_VAPID_KEY=…   # Console Web Push certificate (public)

# Server — Firebase Admin SDK send
AUTH_FIREBASE_PROJECT_ID=…
AUTH_FIREBASE_CLIENT_EMAIL=…
AUTH_FIREBASE_PRIVATE_KEY="…"
DB_BACKEND_MODE=k8s-postgres-fcm`}

  Dual-stack uses `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` as runtime secrets (`webpush-service.ts`, `GET /api/push/vapid-public`). Rebuild the image whenever you rotate `NEXT_PUBLIC_FIREBASE_VAPID_KEY`. See [Push notifications (FCM)](/docs/features/push-notifications-fcm.md).

### Database Schema

{`CREATE TABLE notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  type VARCHAR(50) NOT NULL,
  title VARCHAR(255) NOT NULL,
  message TEXT NOT NULL,
  data JSONB,
  priority VARCHAR(20) DEFAULT 'normal',
  status VARCHAR(20) DEFAULT 'unread',
  created_at TIMESTAMP DEFAULT NOW(),
  read_at TIMESTAMP,
  expires_at TIMESTAMP
);

-- User preferences
CREATE TABLE notification_preferences (
  user_id UUID PRIMARY KEY REFERENCES users(id),
  email_notifications BOOLEAN DEFAULT true,
  push_notifications BOOLEAN DEFAULT true,
  preferences JSONB DEFAULT '{}',
  updated_at TIMESTAMP DEFAULT NOW()
);`}

---

*Ring Platform's notification system provides enterprise-grade reliability with real-time delivery, comprehensive analytics, and cross-platform compatibility.*
