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 notificationWeb 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 keySubscription 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:
- Client generates keys: P-256 key pair on subscription
- Server encrypts: Using client’s public key + shared secret
- Push service cannot read: Only forwards encrypted data
- 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:
- Browser reports expiration: Via
expirationTimefield - Push service rejects: HTTP 410 Gone response
- 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
- Push Notifications API - REST API endpoints
- Settings API - Notification preferences
- WebSocket API - Real-time notifications