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 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. 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_hub route in 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 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. 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →