How Vaultwarden's WebSocket Notification System Works for Real-Time Vault Updates
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 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), 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 changessend_folder_update: Propagates folder renames or deletionssend_auth_request: Delivers login approval requestssend_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. 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:
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_hubroute insrc/api/notifications.rsauthenticates JWT tokens and storesmpsc::Senderchannels in a globalWS_USERSDashMap keyed by user UUID. - Message Format: All notifications use MessagePack encoding via
create_update, following Bitwarden's "ReceiveMessage" protocol with type1and binary length-prefix framing. - Heartbeat Mechanism: The server sends Ping frames every 15 seconds using
create_pingto keep connections alive through proxies and NAT. - Multi-Device Sync: The
WebSocketUsers::send_updatemethod broadcasts to all active sender channels for a user, enabling real-time sync across desktop, mobile, and browser clients. - Anonymous Support: The
anonymous_websockets_hubprovides isolated channels for login approval flows without requiring full authentication. - Reliability Fallback: When
CONFIG.enable_websocket()is false, the system routes notifications throughsrc/api/push.rsfor 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. 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) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →