OpenVireo is a fork of KeePassXC, the offline password manager. KeePassXC is excellent at one thing: a single encrypted .kdbx file on one machine. OpenVireo keeps that app working exactly as before and adds what it never had: accounts, live sync across devices, shared vaults, organizations, and service accounts for CI. All of it is end-to-end encrypted, and the data stays in the standard KeePass format.
I designed the system and its protocol, and built it across ten repositories in four languages: a C++/Qt desktop app, a Python auth server, a C++ sync server, a TypeScript SDK and web app, and a Swift iOS app.
Three ways to use it
Local
Plain files, no network. KeePassXC as it always was.
Self-hosted
Run the sync server yourself, with one token per device. No account needed.
Account
The hosted service: sign-in, sharing, organizations, service accounts.
System architecture
In account mode there are two servers with deliberately separate jobs. The auth server knows who people are and who may open which vault, and it stores keys it cannot use. The sync server stores each vault as one encrypted file and pushes change events. Every client does its own encryption and decryption.
Sign-in starts with discovery (/.well-known/openvireo-configuration), then OPAQUE against the auth server. The auth server returns a 10-minute JWT that lists every vault the holder may open, along with the user's wrapped keys. The client downloads vaults from the sync server, opens them locally, and holds an event stream open for changes from other devices. When access changes, the auth server pushes an event through the sync server, and clients refresh their token.
Key hierarchy
Nothing the servers hold is useful on its own. Each layer of keys is wrapped by the one above it, and the top of the chain exists only on the user's devices.
Sharing is a matter of sealing the vault key once more, to the new member's public key. Removing someone means a client makes a new vault key, seals it to everyone who remains, and re-encrypts the file, because only a client ever holds the plaintext.
Sync and conflicts
The server can't read vaults, so it can't merge them either. Writes use optimistic locking on ETags, and merging happens on the client: a three-way merge per entry, field by field, against the last version both sides had. A same-field conflict keeps the losing value in the entry's history, so nothing is lost.
Screenshots
Desktop · Sign in, self-host, or stay localDesktop · Health check
iOS · All itemsiOS · Entry with one-time codeiOS · Organization membersiOS · Emergency Kit at sign-up
Platforms
DesktopC++ / Qt
macOS · Windows · Linux
KeePassXC core plus accounts, sharing, orgs and live sync. Signed builds (Apple notarization, Azure, GPG) with Sparkle / WinSparkle auto-update.
WebNext.js / TypeScript
Any browser
Account vaults and local .kdbx files in the browser, with offline access and auto-lock.
iOSSwift
iPhone · iPad
Native port of the client protocol, with an AutoFill extension and offline unlock. Live on the App Store.
CLI & CITypeScript SDK
Terminal · GitHub Actions
The ov CLI, openvireo-connect and a GitHub Action, so service accounts can inject secrets into builds.
BrowserWebExtension
Chrome · Firefox · Edge
Fills logins and passkeys from the desktop app over native messaging.
ServersPython / Flask · C++ / Drogon
Hosted or self-hosted
An auth server for identity and access, and a sync server that stores only ciphertext. The sync server also runs on its own.
Design decisions
The server never sees secrets
Everything is end-to-end encrypted, vault names included. A full breach of both servers exposes metadata, never passwords.
OPAQUE for sign-in
An asymmetric PAKE: the password never leaves the client, not even at registration. Two OPAQUE implementations (libopaque in C, opaque-ke in Rust/WASM) are interop-tested against each other.
A Secret Key as well as the password
The account key mixes the master password (Argon2id) with a 128-bit device-generated Secret Key, so a stolen database cannot be cracked offline, even for weak passwords.
A vault is a whole KDBX file
Each vault is a standard KDBX 4 file, encrypted with a random 256-bit vault key. That reuses KeePass merging and history, and an exported vault still opens in any KeePass client.
Sharing by sealed keys
The vault key is sealed to each member's X25519 public key (XChaCha20-Poly1305). Public keys are signed, and share dialogs show fingerprints to compare out of band.
Removing a member rotates the key
A client re-encrypts the vault under a new key. A pending key version sits beside the current one, so a crash mid-rotation never leaves a vault unreadable.
Optimistic writes, client-side merge
Uploads carry If-Match. On 412 the client downloads, runs a three-way merge and retries. The C++ and TypeScript merges are tested against the same recorded results.
Short-lived JWTs between two servers
The auth server signs 10-minute EdDSA tokens listing vault access. The sync server checks them offline against the JWKS, so neither server calls the other on the request path.
How it was built
Four implementations (Python, C++, TypeScript and Swift) have to agree byte for byte on the crypto, the wire format and the merge. So the contract came first: a written spec, OpenAPI files for both servers, shared test vectors and a fixture vault, frozen before any implementation began. Each repository was then built against it in parallel. Every client has to pass the same vectors, so drift shows up as a failing test, not a corrupted vault.
Around that sit signed, auto-updating releases on three desktop platforms, nightly and stable channels, a k6 load test against the full stack, and developer docs for the SDK and the self-hostable sync server.
Load test
k6 drives the whole stack with freshly seeded accounts. Each iteration is one client sync check: list vaults on both servers, a conditional download (mostly 304), a 200 KB upload one time in five, and a token refresh when the access token gets old. Every virtual user comes from its own IP, so per-IP rate limits apply as in production, and each container is capped to a small slice of CPU (auth at half a core, sync at 0.15) to approximate a small production host.
Ramp to 120 syncs/s
Ramp to 600 syncs/s
Requests
37,934, 0 errors
91,647, 0 errors
Auth p95
12 ms
1.2 s (saturated)
Sync p95
1.2 ms
1.2 ms
Sync CPU / RAM
6% / 33 MB
11% / 103 MB
The Python auth server (two gunicorn workers) saturates at about 190 req/s on half a core. The C++ sync server holds 1.2 ms p95 throughout on 0.15 of a core, and Postgres is barely loaded. Most auth traffic is token refreshes, so auth is the part to scale out first; for vault storage, disk space for version history runs out before CPU does.