Sync and conflicts
An environment’s entries are encrypted as a single blob and stored with a version number. Every write increments it.
Optimistic concurrency
Section titled “Optimistic concurrency”You pull, you edit, you write back — and the write says which version you started from. If someone else wrote first, the server rejects yours instead of overwriting theirs.
At that point your client re-fetches, and merges.
The merge is three-way, on your device
Section titled “The merge is three-way, on your device”The server cannot help here: it cannot read any of the three versions. So merging happens locally, comparing your base snapshot, your edits, and what is now on the server.
Most of the time this resolves with nothing to ask you:
| Situation | Result |
|---|---|
You changed A, they changed B |
Both applied |
You both changed A identically |
Applied once |
They added C, you didn’t touch it |
Kept |
You deleted D, they didn’t touch it |
Deleted |
You both changed A differently |
You are asked |
It never picks for you
Section titled “It never picks for you”That last row is the one that matters, and the rule is absolute: when two people changed the same key to different values, Cendarum stops and asks.
Last-writer-wins would be much simpler. It is also how a staging database URL ends up in production and nobody notices until the pager goes off. A silent wrong value in a secrets manager is an outage with no audit trail pointing at the cause.
The conflict prompt shows you which key, each side’s type, and whether the notes differ. It does not show the values side by side, and does not print them to a log.
Whole entries, not fields
Section titled “Whole entries, not fields”The unit of merge is the entire entry — value, type, and note together.
That prevents a specific bad outcome: someone re-types an entry from secret to
env in the same change that alters its value. Merged field by field, you could
end up with the new value quietly rendering unmasked. Merged as a unit, the
composite always reaches a human with both sides’ type visible.
Version markers
Section titled “Version markers”Your client remembers the highest version it has successfully authenticated for each environment, and refuses to accept anything older. A server that tried to serve you a stale payload — replaying yesterday’s secrets after you rotated a compromised key — gets refused rather than believed.
That marker is stored durably, so it survives a restart. If the file is corrupt, the client fails closed instead of resetting the marker, because a silent reset is exactly what would reopen the replay window.
One limit worth naming: two honest writes can legitimately exist at the same version — the loser of a rejected write and the winner’s. A malicious server could use that to show different members different histories, as long as version numbers only ever grow. Detecting it needs each payload to be chained to its predecessor, which is a change to the encryption format rather than something we can patch around. It is listed in the threat model.
From the CLI
Section titled “From the CLI”cendarum import participates in the same loop, and on conflict it stops:
cendarum: error: import conflicts with concurrent changes — resolve in the desktop app:cendarum: conflicting key: API_URLKey names only. The CLI ships no resolution interface on purpose — choosing between two versions of a secret needs both values visible and a person looking at them, and a terminal prompt is a poor place to make that call.
Version history
Section titled “Version history”Each environment keeps its payload history, and you can fetch an earlier version to see what a value used to be.
With one caveat: history from before a key rotation is unreadable, because the key that decrypted it was replaced. That is the same property that makes revocation real — the old key is genuinely gone — and it is the accepted cost of it.