# How Vaultwarden's WebSocket Notification System Works for Real-Time Vault Updates

> Discover how Vaultwarden's WebSocket notification hub delivers real time vault updates instantly through MessagePack payloads stored in a global DashMap.

- Repository: [Daniel García/vaultwarden](https://github.com/dani-garcia/vaultwarden)
- Tags: internals
- Published: 2026-03-07

---

**Vaultwarden uses a WebSocket notification hub that stores per-user sender channels in a global `DashMap` and broadcasts MessagePack payloads instantly when vault objects change.**

The **WebSocket notification system** in Vaultwarden enables real-time synchronization of vault data across clients without polling. When a user modifies a cipher, folder, or send object, connected devices receive immediate updates through persistent WebSocket connections. This system is implemented primarily in [`src/api/notifications.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/notifications.rs) and uses Tokio channels paired with MessagePack encoding to deliver low-latency notifications.

## WebSocket Connection Lifecycle

Clients initiate real-time updates by establishing a persistent connection to the notification hub. The server authenticates the request and maintains the connection until the client disconnects or the network fails.

### Client Authentication and Hub Registration

When a client opens a WebSocket connection to `GET /api/notifications/websocket/hub`, the server validates the JWT access token provided in the query parameters. Upon successful authentication, the `websockets_hub` function generates a unique entry UUID and creates a `tokio::sync::mpsc::Sender<Message>` channel. This pair is stored in a global `WebSocketUsers` map keyed by the user's UUID.

The storage structure uses `DashMap<String, Vec<(Uuid, Sender<Message>)>>`, allowing multiple concurrent connections per user across different devices. A `WSEntryMapGuard` automatically removes the entry from `WS_USERS.map[user_id]` when the connection drops, preventing memory leaks.

### Connection Management and Heartbeats

Inside the `Stream!` macro (lines 44-84 of [`src/api/notifications.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/notifications.rs)), the server simultaneously listens for three events: inbound client messages, outbound channel messages (`rx.recv()`), and a 15-second ping interval. The ping mechanism sends `Message::Ping(create_ping())` where `create_ping` returns a MessagePack array `[6]` (Ping type). Clients must respond with Pong frames to maintain the connection.

The server also expects an initial MessagePack handshake message (`{protocol:"messagepack",version:1}`) and responds with `INITIAL_RESPONSE` before entering the main message loop.

## Global User Registry Architecture

The `WebSocketUsers` struct serves as the central registry for all active notification streams. Implemented as a `LazyLock` singleton named `WS_USERS`, this global state holds the mapping between user UUIDs and their active sender channels.

When broadcasting updates, the system iterates over all `Sender<Message>` instances registered for a specific user UUID. This design enables multi-device synchronization: a change made on a mobile device instantly propagates to desktop and browser extensions via their respective WebSocket channels.

## Broadcasting Real-Time Updates

When vault data changes, Vaultwarden triggers notification methods that construct and deliver payloads to connected clients.

### MessagePack Payload Construction

The `create_update` function (lines 75-90) builds a Bitwarden-compatible "ReceiveMessage" array following this structure:

- Type: `1`
- Headers: empty map
- ID: `nil`
- Target: `"ReceiveMessage"`
- Arguments: map containing the payload object

The `serialize` helper encodes this structure using MessagePack with a length prefix compatible with official Bitwarden clients. This binary format reduces bandwidth compared to JSON while maintaining compatibility with the Bitwarden protocol.

### Send Methods for Different Vault Objects

The `WebSocketUsers` implementation provides specialized methods for different vault entities:

- **`send_cipher_update`**: Notifies clients of password or secure note changes
- **`send_folder_update`**: Propagates folder renames or deletions
- **`send_auth_request`**: Delivers login approval requests
- **`send_send_update`**: Updates for Bitwarden Send objects

Each method first checks `NOTIFICATIONS_DISABLED` (true when both WebSockets and Push are disabled). If WebSockets are enabled via `CONFIG.enable_websocket()`, the method calls `self.send_update`, which iterates through the user's sender channels and transmits `Message::binary(data)`.

## Anonymous Hub for Authentication Requests

In addition to the authenticated hub, Vaultwarden exposes `anonymous_websockets_hub` (lines 90-112) for scenarios requiring unauthenticated access. This endpoint uses `AnonymousWebSocketSubscriptions` to store a single sender per token rather than per user UUID.

The anonymous hub exclusively handles authentication request responses, allowing new device logins to receive approval notifications without requiring a full vault session. When an admin approves a login request, the system pushes the response only to the specific anonymous connection associated with that pending auth token.

## Fallback to Push API

When WebSockets are disabled via configuration or network constraints prevent direct connections, Vaultwarden falls back to the **Push API** implemented in [`src/api/push.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/push.rs). The same high-level notification helpers (`send_cipher_update`, `send_folder_update`, etc.) check `CONFIG.push_enabled()` and invoke corresponding `push_*` functions (e.g., `push_cipher_update`).

This dual-path architecture ensures that real-time updates reach mobile devices even when background WebSocket connections are terminated by operating system power management. The Push API uses platform-specific services (Firebase Cloud Messaging for Android, Apple Push Notification Service for iOS) to wake sleeping clients with encrypted update notifications.

## Client Implementation Example

The following JavaScript demonstrates connecting to the notification hub from a browser or Electron application:

```javascript
const token = "<access-token>";
const ws = new WebSocket(`wss://${location.host}/api/notifications/websocket/hub?access_token=${token}`);

ws.binaryType = "arraybuffer";

ws.onopen = () => console.log('WebSocket connected');

// Decode MessagePack payload using msgpack-lite or similar
ws.onmessage = (event) => {
  const data = new Uint8Array(event.data);
  const msg = msgpack.decode(data);
  
  // msg[4][0].Payload contains the vault update
  if (msg[4] && msg[4][0] && msg[4][0].Payload) {
    console.log('Vault update received:', msg[4][0].Payload);
    // Update local vault cache here
  }
};

ws.onclose = () => console.log('WebSocket closed');

```

Clients should handle binary MessagePack frames, monitor for Ping type `[6]` messages to verify connection health, and gracefully reconnect when the connection drops.

## Summary

- **Connection Registration**: The `websockets_hub` route in [`src/api/notifications.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/notifications.rs) authenticates JWT tokens and stores `mpsc::Sender` channels in a global `WS_USERS` DashMap keyed by user UUID.
- **Message Format**: All notifications use MessagePack encoding via `create_update`, following Bitwarden's "ReceiveMessage" protocol with type `1` and binary length-prefix framing.
- **Heartbeat Mechanism**: The server sends Ping frames every 15 seconds using `create_ping` to keep connections alive through proxies and NAT.
- **Multi-Device Sync**: The `WebSocketUsers::send_update` method broadcasts to all active sender channels for a user, enabling real-time sync across desktop, mobile, and browser clients.
- **Anonymous Support**: The `anonymous_websockets_hub` provides isolated channels for login approval flows without requiring full authentication.
- **Reliability Fallback**: When `CONFIG.enable_websocket()` is false, the system routes notifications through [`src/api/push.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/push.rs) for delivery via platform push services.

## Frequently Asked Questions

### How does Vaultwarden handle multiple concurrent connections from the same user?

Vaultwarden stores a `Vec<(Uuid, Sender<Message>)>` for each user UUID in the global `WS_USERS` map. When `send_update` is called, it iterates through all sender channels in the vector and broadcasts the MessagePack payload to every connected device. This ensures that updating a password on a mobile phone instantly reflects on the user's desktop application and browser extension.

### What happens when WebSockets are disabled in the configuration?

If `CONFIG.enable_websocket()` returns false and `NOTIFICATIONS_DISABLED` is not set, Vaultwarden routes notifications through the Push API defined in [`src/api/push.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/push.rs). The same helper methods (`send_cipher_update`, `send_folder_update`, etc.) invoke `push_*` functions that deliver updates via Firebase Cloud Messaging or Apple Push Notification Service instead of direct WebSocket channels.

### Why does Vaultwarden use MessagePack instead of JSON for WebSocket messages?

MessagePack provides a binary serialization format that is more compact than JSON while maintaining schema compatibility with the official Bitwarden clients. The `create_update` function constructs a specific array structure `[type, headers, id, target, args]` that the Bitwarden client expects, and the `serialize` helper adds a length prefix to the binary payload to match the protocol specification used across the Bitwarden ecosystem.

### How does the anonymous WebSocket hub differ from the authenticated hub?

The `anonymous_websockets_hub` endpoint (lines 90-112 of [`src/api/notifications.rs`](https://github.com/dani-garcia/vaultwarden/blob/main/src/api/notifications.rs)) does not associate connections with a user UUID. Instead, it uses `AnonymousWebSocketSubscriptions` to map tokens to single sender channels. This design supports authentication request flows where a new device needs to receive login approval notifications without having established a full vault session or possessing a user-scoped access token.