The threat model
Ubimate assumes the machine in front of you is yours and the server is not. Everything follows from that.
What the design defends against: a hosting provider reading your documents, an operator of the sync service being compelled to hand them over, a stolen backup of the server's database, and a compromise of the server process itself. In each of those cases the attacker holds ciphertext and wrapped keys, and the material that would open them was never there.
What it does not defend against: someone with access to your unlocked device, malware running as you, or a passphrase weak enough to guess. Local data is deliberately stored unencrypted so that nothing — not even a forgotten passphrase — can lock you out of it; the protection there is the operating system's, so use full-disk encryption on anything that travels.
Local mode
The desktop app has no server in it. Documents, Yjs history and media live in a SQLite database and a folder on your disk, and every feature — editing, search, tables, panels, the HTML mirror — runs against that copy.
No account exists, nothing is transmitted, and there is no cryptography in the way: the local database is plain, on purpose. Encryption needs a key, and a key can be lost, so there is none here — whatever happens to a passphrase, your own content on your own disk stays yours to open. Back it up as a file, and the HTML mirror keeps every page readable without Ubimate at all.
If you never turn on sync, nothing in the rest of this page applies to you. There is no server holding anything of yours to protect.
Keys and authentication
Your keys are derived from your credentials on your own device, and derived the same way every time, so there is no keypair to store, sync or recover.
email address (permanent, chosen at registration) │ ▼ UUID(SHA-256(normalize(email))) user_id → used as the Argon2id salt │ passphrase ┤ ▼ Argon2id (client-side, high cost) 32-byte seed ├─▶ Ed25519 signing keypair (who you are) └─▶ X25519 agreement keypair (what you can open)
Logging in is a signature, not a secret: the server issues a single-use nonce with a short lifetime, your device signs it with the Ed25519 private key, and the server checks that against the public key it holds. It stores that address, your two public keys, a display name and a contact address — no passphrase, no hash, nothing that could be cracked offline.
The registration address is permanent because it is an input to the derivation: change it and you change the keys, and everything sealed under the old ones stops opening. That is why the contact address is a separate field, and why a relay address is a good idea — Apple's Hide My Email or any forwarder you control keeps the permanent one from being an address you read mail at. The contact address starts out as the one you registered with; changing it is coming in a later version.
The cost of having no back door
A forgotten passphrase cannot be reset into readable data. Your local workspace is unaffected and can be re-synced under new credentials, but content that exists only in the cloud, under a passphrase nobody has, stays closed. There is no recovery path on our side, because a recovery path we could use is a recovery path we could be compelled to use.
Content encryption
Each Space has its own random symmetric content key. Everything bound for the server — document properties, Yjs updates, attachments — is encrypted with it on your device before it is sent.
The content key itself never travels in the clear. It is sealed to each
member's X25519 public key with crypto_box_seal and stored in
that wrapped form, so a member unwraps their own copy locally and the server
holds envelopes it has no key for.
Collaboration without a trusted middle
Sharing a Space with other people is not in the app yet: it comes after the sync service launches, with the Pro plan. This is how it is built. Live editing uses CRDTs (Yjs), which converge without anything in the middle understanding the content. The relay moves encrypted update blobs between clients, stores them as it received them, and merges nothing it can read. Compaction — folding many updates into one snapshot — is CRDT arithmetic on opaque state, not a read.
Inviting someone means sealing the Space key to their public key, which the sender signs so the recipient can verify where the invitation came from. Removing a member removes their access; rotating a Space key after a revocation is on the roadmap rather than automatic today.
What the server sees
Encryption hides content, not the existence of things. These are the fields the sync service necessarily observes, and why each one is there.
| Visible | Why it has to be |
|---|---|
| Document tree: ids, parent, type, position | Routing and ordering. The type is the one you chose when you made the block, not something inferred from what is in it. |
| Timestamps | Sync ordering and conflict resolution. Sorting and filtering by them happens on your device. |
| Document count and blob sizes | Storage accounting and upload progress. Sizes leak rough complexity — a short note and a video attachment do not look alike. |
| Space and user ids, your display name and your contact address | Access control, and how collaborators find the public key to seal a Space key to. |
| Membership and permission level | Authorization. That two accounts share a Space is visible; what is in it, and which document either is editing, is not. |
What is never visible: page content, titles, icons, tags and other properties, datatable rows and schemas, attachment contents, your search index, your passphrase, your seed, and your private keys.
The trade is deliberate. Encrypting the structure as well would mean a server that cannot route a request, enforce a permission or tell one document from another — and the features that depend on the server knowing that much are the ones people install sync for.
Server hardening
Zero knowledge is not a substitute for the ordinary work, since a server that cannot read your documents can still be abused.
- Challenge-response authentication with single-use, short-lived nonces and replay protection.
- Rate limiting on the authentication endpoints, per IP.
- HttpOnly, SameSite=strict session cookies for browsers; short-lived bearer tokens for the desktop app, and 60-second tokens for the sync socket.
- A JWT secret that must be set: the server refuses to start in production without one rather than falling back to a default.
- Prepared statements throughout, and a separate SQLite database per user.
- Uploads served behind authentication, with a path-traversal guard.
- Link previews fetched behind an SSRF guard that re-checks every redirect hop against private and loopback ranges.
- HTTP security headers via helmet, and dependencies pinned and audited.
Known gaps
The list a security page usually leaves out. These are real, and none of them is a reason not to say so.
- Metadata inference. Someone with long-term access to the server could correlate timestamps, update frequency and blob sizes into a picture of when you work and roughly on what. There is no padding or cover traffic today.
- No external audit. The design is documented and the server's source is published, but no third party has reviewed it. We would like one, and we will publish the report when there is one to publish.
- Passphrase change, reset and key rotation. Changing a passphrase means re-wrapping every Space key under the new public key, resetting a forgotten one means doing that from a device still signed in, and revoking a collaborator should rotate the Space key. All three are planned rather than shipped.
- Guests in cloud Spaces. Server-side Space access for guests is not yet fully zero-knowledge; where it applies, it is called out in the product rather than glossed over here.
- Link previews reach out from your device. On desktop the app fetches the linked page itself rather than asking the server to, so the URL never reaches our infrastructure — and the site you linked sees your address rather than ours. The address itself is checked the same way at both ends: resolved before anything is dialled, refused if it is private, and checked again on every redirect.
- Local data is not encrypted. By design, and stated again because it is the trade-off most worth understanding: sovereignty on your disk, protected by your operating system rather than by us.
Self-hosting
The sync and collaboration backend — Express, the Yjs relay and SQLite
— is published in full at
github.com/ubimate/ubimate-server
and runs with a single docker compose up. It is source-available
under the Elastic License 2.0 rather than open source under an OSI-approved
license: you may read it, audit it and run it for yourself or your company,
but not resell it as a hosted service.
The point is not only privacy, which the encryption already covers. It is that the arrangement survives us: if this company stops existing tomorrow, everything you rely on keeps running on hardware you control, from a repository you already have.
Our hosted service runs on servers in the EU, reachable only by key-based SSH, with continuous encrypted backups of each node's database.
Reporting a vulnerability
If you have found something, please tell us before you tell everyone else. Send it to security@ubimate.com with what it is, what it lets an attacker do, and enough detail to reproduce it.
We aim to acknowledge a report within two business days and to give you a remediation timeline within seven, and we ask for 90 days before public disclosure.
Scope
- The desktop app, on macOS, Windows and Linux
- The mobile app
- The web app and the hosted API and sync server
- This website
Out of scope: denial of service against our infrastructure, already-public vulnerabilities in third-party dependencies, and social engineering of the people who work here.
Good faith
We will not pursue researchers who act in good faith, give us enough to reproduce and fix the issue, and do not access or modify anyone's data beyond what it takes to demonstrate the problem.
Vulnerabilities
Everything else
The server, in full