Push Notifications

Overview

Cloudillo implements Web Push notifications using the VAPID (Voluntary Application Server Identification) protocol. Push notifications are sent when users receive actions while offline or not connected via WebSocket.

Architecture

User Action Created
       ↓
Action Forwarding Decision
       ↓
Is recipient connected via WebSocket?
  ├─ Yes → Send via WebSocket (real-time)
  └─ No → Send Push Notification
              ↓
         Lookup user's push subscriptions
              ↓
         Encrypt notification payload
              ↓
         POST to push service endpoint
              ↓
         Browser receives notification

Web Push Standards

The implementation follows these RFCs:

RFC Title Purpose
RFC 8292 VAPID for Web Push Server identification
RFC 8188 Encrypted Content-Encoding for HTTP Payload encryption
RFC 8291 Message Encryption for Web Push End-to-end encryption

VAPID Keys

Each tenant has a VAPID key pair for authenticating with push services:

  • Private key: Stored securely in the database
  • Public key: Shared with clients for subscription

VAPID keys are automatically generated on first request if they don’t exist.

Key Management

Client requests VAPID public key
       ↓
Server checks for existing key
       ↓
Key exists? → Return public key
       ↓
No key? → Generate new P-256 key pair
          Store in database
          Return public key

Subscription Flow

sequenceDiagram
    participant C as Client
    participant SW as Service Worker
    participant S as Cloudillo Server
    participant PS as Push Service

    C->>S: GET /api/notification/vapid-public-key
    S-->>C: {vapidPublicKey: "BM5..."}

    C->>SW: pushManager.subscribe({userVisibleOnly: true, applicationServerKey})
    SW->>PS: Subscribe request
    PS-->>SW: PushSubscription
    SW-->>C: PushSubscription

    C->>S: POST /api/notification/subscription {subscription}
    S-->>C: {id: 12345}

Push Delivery

When an action is received for an offline user:

sequenceDiagram
    participant A as Action Sender
    participant S as Cloudillo Server
    participant PS as Push Service
    participant B as User's Browser

    A->>S: POST /api/inbox {action}
    S->>S: Process action
    S->>S: Check if recipient is online

    alt User is online (WebSocket connected)
        S->>B: WebSocket message
    else User is offline
        S->>S: Load push subscriptions
        S->>S: Encrypt payload with user's public key
        S->>PS: POST {encrypted payload}
        PS->>B: Push notification
        B->>B: Display notification
    end

Notification Types

Users can configure which notification types they receive:

Setting Default Description
notify.push.message true New direct messages
notify.push.mention true Mentioned in content
notify.push.reaction false Reactions to your content
notify.push.connection true Connection requests
notify.push.follow true New followers

Settings are stored per-user and checked before sending notifications.

Encryption

Push notification payloads are encrypted end-to-end:

  1. Client generates keys: P-256 key pair on subscription
  2. Server encrypts: Using client’s public key + shared secret
  3. Push service cannot read: Only forwards encrypted data
  4. Client decrypts: Using private key in browser

Encryption Parameters

Parameter Description
p256dh Client’s P-256 public key
auth 16-byte authentication secret
content-encoding aes128gcm

Payload Structure

{
  "type": "action",
  "actionType": "MSG",
  "issuer": "alice@example.com",
  "preview": "New message from Alice",
  "actionId": "a1~xyz789..."
}
Field Description
type Notification type (action, system)
actionType Action token type (MSG, CONN, FLLW)
issuer Action issuer identity
preview Short preview text
actionId Action ID for deep linking

Service Worker

The client service worker handles incoming push events:

self.addEventListener('push', function(event) {
  const data = event.data.json()

  const options = {
    body: data.preview,
    icon: '/icons/notification.png',
    badge: '/icons/badge.png',
    data: { url: `/action/${data.actionId}` }
  }

  event.waitUntil(
    self.registration.showNotification('Cloudillo', options)
  )
})

self.addEventListener('notificationclick', function(event) {
  event.notification.close()
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  )
})

Subscription Management

Multiple Devices

Users can have multiple push subscriptions (one per device/browser):

  • Each subscription has a unique ID
  • All subscriptions receive notifications
  • Expired subscriptions are automatically cleaned up

Subscription Expiration

Push subscriptions can expire:

  1. Browser reports expiration: Via expirationTime field
  2. Push service rejects: HTTP 410 Gone response
  3. User unsubscribes: Manual deletion

Expired subscriptions are removed automatically.

Error Handling

Push Service Response Action
201 Created Success
400 Bad Request Log error, don’t retry
410 Gone Remove subscription
429 Too Many Requests Retry with backoff
5xx Server Error Retry with backoff

See Also