Stephan Sergeev283 downloadsSync your Obsidian vault between desktop, iOS and Android through your own server. Self-hosted, end-to-end encrypted, free and open source: no cloud, no subscription.
Two-way sync for your vault between desktop, iPhone, iPad and Android, through a server you own. Self-hosted, end-to-end encrypted, free and open source. No subscription, no third-party cloud, no account: one small binary on a machine you already have, and your notes.
If you have looked at Obsidian Sync, Remotely Save, Self-hosted LiveSync, Syncthing or Git for this, the difference is below.
Every self-hosted sync asks for something first. LiveSync wants a CouchDB. Synch and Osync want Docker or a Cloudflare account. Remotely Save wants the cloud storage you were trying to leave. Lockstep wants one file on one machine you already have.
And it shows its work. A suite of data-loss scenarios runs on every commit, driving the real plugin against a real server: simultaneous edits, deletions racing edits, renames racing everything, connections cut mid-file, the app killed mid-write, clocks set wrong, Unicode in every form. The promise is narrow and testable: no version of your text disappears quietly.
desktop your server phone
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Obsidian │ │ lockstep │ │ Obsidian │
│ plugin │ ──changes─▶ │ SQLite + │ ◀─changes── │ plugin │
└──────────┘ │ content │ └──────────┘
└──────────────┘
The server is an ordered log and a store of bytes. All the judgement lives in the plugin. Devices exchange deltas, never directory walks. A deletion is a tombstone. A rename travels as a rename, not as a delete plus a stranger.
Edits to different parts of a note merge silently. Edits to the same lines keep both versions whole and ask you, right in the notice, with each side named after the device it came from. An edit always beats a deletion. Deleted files stay recoverable from any device for thirty days (yours to change), then the server erases them.
Syncing is automatic: moments after you stop typing, when the app goes to the background, and every fifteen seconds while it is open.
A machine reachable from the internet, and a domain name pointed at it:
curl -fsSLO https://github.com/stephansergeev/obsidian-lockstep-sync/releases/latest/download/install.sh
less install.sh # read what you are about to run as root; q to close
sudo bash install.sh sync.example.com
It installs the binary, creates an unprivileged user, arranges TLS around whatever already runs on the machine, and prints a link that sets up your first device. Run it again to update; your data is left alone.
Already have a vault? sync-server import --from ~/Vault. Want space back?
sync-server gc --dry-run shows what old history it would drop; nothing is collected
without being asked. No domain? A private network like Tailscale works: point the
plugin at the private address and skip TLS entirely.
There is a docker-compose.yml for machines that only run
containers. Read the volume line before starting it: it is a bind mount on purpose,
because a named volume is what docker compose down -v destroys.
A member of the community wrote an excellent guide for running the container and issuing a device token on a Ugreen NAS (UGOS), where you cannot open a terminal inside the container: How to set up the Docker container and generate a device token.
One server holds any number of vaults. Each is its own database and blob store, made on first use, and every device carries a token bound to one of them:
sync-server token add --vault work --name laptop # creates the vault on first use
sync-server token list # every vault, with a VAULT column
sync-server token list --vault work # just this one
sync-server link --vault work --url https://sync.example.com --name laptop
sync-server token revoke --name laptop --vault work # one vault; drop --vault for all
Same URL for all of them, and each vault has its own passphrase and key. In Docker,
run these against the container and let it use its default /data (the path your bind
mount provides), rather than passing --data:
docker run --rm -v /your/data:/data ghcr.io/stephansergeev/obsidian-lockstep-sync:latest \
token add --vault work --name laptop
Settings → Community plugins → Browse → Lockstep Sync → Install → Enable. Or from the web: community.obsidian.md/plugins/lockstep-sync.
First device: open the link the installer printed. Every next device: Link a new device in the settings shows a QR code; the page it opens walks through the steps.
Type a passphrase in the settings and notes are sealed on the device with AES-256-GCM before they leave, file and folder names included. The key comes from the passphrase through Argon2id and goes nowhere. A wrong passphrase stops syncing; it never falls back to plaintext.
It can be turned on later, on a vault already on the server. The plugin uploads every file again, sealed, then erases the readable copies and their history; other devices join with the same passphrase once it is done. If the app is closed halfway, it continues on the next start.
Choose the passphrase as if it were permanent, because it is: set once per vault, never changeable, never recoverable. Losing it does not lose your notes, since devices hold ordinary readable files. It loses the server copy, its history, and the ability to set up new devices from it. Four unrelated words beat eight clever characters, and the plugin says so while you type.
Releases are built by CI from the tagged commit and ship SHA256SUMS. Tags are
signed. The server sees ciphertext under ciphertext names and answers only to
per-device revocable tokens. Details and the threat model: SECURITY.md.
The protocol and its reasoning: spec/.
MIT, the whole repository. Written by Stephan Sergeev.
Built by Stephan Sergeev. More at stephansergeev.com.