What Nectenda's server can and cannot see
Nectenda is end-to-end encrypted: the server stores an append-only log of ciphertext it has no key for. This document says exactly what that does and does not cover, because a privacy claim you cannot check is worth very little.
Every claim below names the code that implements it. If a claim and the code ever disagree, the code is right and this document is a bug.
Scope. This describes the cryptography and what the server holds. It is not a promise about a particular deployment's logging, retention or jurisdiction; those belong in a privacy policy and are not verifiable from code by anyone.
The short version
| Server sees | |
|---|---|
| Note content | No — AES-256-GCM ciphertext only |
| Note and folder paths | No — document ids are HMACs of the path |
| Attachment content | No — sealed in a chunked AEAD envelope |
| Attachment filenames | No — they live inside the encrypted listing |
| Your password | No — never sent; not even in hashed-and-then-hashed form |
| Your private key | No — stored wrapped, unwrappable only by your password |
| Folder display names | No — sealed under the folder's content key |
| Who shares a folder with whom | Yes |
| Sizes, timing, device and account records | Yes |
Why you do not have to trust the server
The keys never reach it.
Your password never leaves the device. It is stretched with PBKDF2-SHA256
at 600,000 iterations into a master key (deriveMasterKey,
packages/shared/src/crypto.ts). The master key is never sent and never
stored. What the server receives at login is a second, independent
derivation, deriveAuthHash, which cannot be used to derive the encryption
key — and the server bcrypts even that before storing it, because it is still
password-equivalent for logging in.
Your identity key is wrapped before it is uploaded. A P-256 ECDH keypair is
generated on device; the private half is encrypted under a key derived from the
master key (deriveEncKey) and only then stored server-side as
users.wrapped_private_key. The server holds a blob it cannot open.
Folder keys are wrapped to each member individually. Sharing a folder means
sealing its content key to the recipient's public key using ECIES over P-256
with a fresh ephemeral keypair per wrap (wrapSecret / wrapFolderKey). The
server stores the results in folder_keys — ciphertext, one row per member. It
never handles an unwrapped folder key.
Folder names are sealed too. A folder called "Redundancy consultation —
legal" said as much as the notes inside it, so shared_folders.name holds
ciphertext under the folder's content key, with name_key_id recording which
generation sealed it (sealFolderName in
packages/plugin/src/folder-crypto.ts). The column is not nullable and the
create route refuses an unsealed name: there is no such thing as a folder whose
name the server can read.
Document ids are HMACs, not paths. A document is addressed as
{folderId}/{HMAC-SHA256(nameKey, relativePath)} truncated to 16 bytes
(deriveDocId). The server cannot reverse it and therefore never learns your
folder structure or note titles. The name key is wrapped to members like any
other folder secret, and is deliberately separate from the content key so that
rotating content keys does not rewrite every id in the log.
Content is encrypted before it is pushed. Each Yjs update is sealed with
AES-256-GCM, IV prepended, 28 bytes of overhead (encrypt). Attachments use a
chunked envelope whose additional authenticated data binds
blobId ‖ chunkIndex ‖ totalChunks ‖ algo ‖ codec, so a reordered, truncated
or substituted chunk is rejected rather than decrypted
(packages/shared/src/blob-cipher.ts).
The attack this does not stop by itself
Encrypting content protects you from a server that reads. It does not, on its own, protect you from a server that actively interferes.
When you add a collaborator, your client asks the server for their public key and wraps the folder key to it. A malicious server could return its own public key instead. You would be sharing with the operator, and everything would look normal.
The defence is fingerprint comparison, and it requires you to act. The
folder members screen shows a fingerprint for every member and for you
(publicKeyFingerprint, used in packages/plugin/src/main.ts). Compare them
with your collaborators through any channel that is not this server — in person,
a phone call, a different messenger. If they match, no key was substituted.
This is the same mechanism as Signal's safety numbers and Threema's QR verification, and it carries the same caveat: it only works if somebody checks. Until then the guarantee holds against an operator who reads, not one who interferes.
What is not encrypted
Stated plainly, because a security document that only lists strengths is marketing.
- The membership graph.
folder_membersrecords which accounts share which folders, and when they were added. - Account and identity records. Username and email, account, plan, and role.
- Device records.
devicesholds a device id, an optional label, and the platform string, with first- and last-seen timestamps. - Sizes and timing. Every update's byte length and arrival time, every attachment's encrypted size, and per-account usage totals.
- Anything the network sees. IP addresses and connection times, as with any service.
- Recovery material. Your master key wrapped under your recovery key, and the parameters needed to derive from it. Both are stored, both are opaque without the key, and the previous set is kept when you change your password rather than being deleted.
Forgetting your password
There is a way back, and it is the recovery key you were shown once at registration. Nothing else works: your password is not stored, and it is not merely checked but used — it derives the key that unwraps everything — so no administrator, and no amount of access to the server, can reset it for you.
Asking to recover discloses nothing until you prove you hold the key. Requesting recovery parameters for a username returns only derivation parameters, and returns plausible ones even for accounts that do not exist, so it cannot be used to find out who has an account here. The wrapped key itself is released only after the server has verified a proof it cannot forge, and failed attempts are counted against the account rather than the network address they arrive from.
Recovering does not change your identity key, so every folder shared with you stays readable afterwards. It does issue a new recovery key, because the old one no longer opens anything — save it as you saved the first.
Traffic analysis is real. Sizes and timing alone can reveal that a document is being actively edited, roughly how large it is, and who was connected at the time. End-to-end encryption does not hide that, here or anywhere else, and no amount of auditing the client will tell you what a server chooses to retain.
If that metadata is what you need to protect, the answer is not cryptography — it is running the server yourself.
What we do not claim
- Not audited. No external security audit has been carried out. The design follows standard constructions and is covered by unit tests, but that is not the same thing and should not be presented as if it were.
- No forward secrecy for stored history. The append-only log is encrypted under the folder's current content key generation. Someone who obtains a content key and a copy of the log can read the history that key covers. Key rotation limits the window; it does not erase the past.
- No protection against a compromised device. Keys live in the OS keychain
where available (
docs/key-storage.md); malware running as you can read what you can read. - Read-only membership does not exist. A member holding the folder content
key can always write, whatever any server-side role says. See
docs/sync-limitations.md— it is a direct consequence of E2EE, and any role we enforce server-side is a convenience, not a security boundary.
Checking this yourself
The client is the whole trust anchor, and unusually for an encrypted product,
you can read the exact code that runs. Obsidian plugins ship as JavaScript:
main.js is on your disk, in your vault, at
.obsidian/plugins/nectenda/main.js. There is no compiled binary and no
reproducible-build problem to reason about — the file that executes is the file
you can inspect.
This is also why there is no Nectenda web app, and will not be one. A web page is delivered fresh by a server on every visit, and can be delivered differently to different people, so "read the code that runs" is not a promise a browser-based product can keep. Everything that touches your password, your master key, your identity key or a folder key happens in the plugin on your disk. Anything of ours you reach in a browser — a marketing page, a checkout, an invoice — never sees any of them, and cannot create or recover an account.
That boundary is a deliberate limit on the product, not an accident of what has been built yet. It is the reason the claim above holds.
Worth checking, in rough order of value:
- That plaintext never leaves. Every push goes through
encryptbefore it reaches the socket — seepackages/plugin/src/multiplexed-provider.ts. - That paths are not sent. Document names are derived by
deriveDocId; grep the traffic for a note title and you will not find one. - That the server's copy is unreadable. Attach to the server's database and
look at
doc_updates.payload. It is opaque without a folder key. - That fingerprints match what your collaborator sees.
That the file you installed is the source you read
The above assumes the main.js in your vault corresponds to the published
source. You do not have to assume it. Every release carries the SHA-256 of its
main.js, and the source it was built from is the repository the release lives
in, at that tag:
git clone https://github.com/Nectenda/nectenda-plugin && cd nectenda-plugin
git checkout <the release tag>
node scripts/verify-build.mjs --built ~/vault/.obsidian/plugins/nectenda/main.js
It rebuilds and compares byte for byte. On a difference it reports the offset and prints both sides around it, because "they do not match" does not distinguish a differing esbuild version from a differing cipher — and those deserve very different reactions.
The licence does not restrict any of this. The plugin is source-available under PolyForm Shield 1.0.0: reading, auditing, building and comparing are expressly permitted. What is withheld is the right to build a competing product, which is not something verification requires. If anything the check matters more now than it did under MIT — having narrowed what you may do with the code, the claim rests entirely on what you can check.
Sources of comparison
Relay, the closest competitor, publishes an equivalent page and states:
"Relay isn't end-to-end encrypted, we have the technical ability to access synced content if required."
The difference is structural rather than a matter of degree, and it is the reason this document can be specific where a trust promise cannot be. Quoted from their documentation on 1 September 2026; check it yourself rather than taking our word for what a competitor says.