Shreekrishna chinta111 downloadsFree, local-first P2P sync for your vault. No cloud, no account.
Free, local-first P2P sync for your Obsidian vault. No cloud. No account. No subscription.
Sync your Obsidian vault between your laptop and your phone over your own network — direct and private. Your notes never touch a third-party server.
Obsidian Sync costs $4/month and sends your vault through Obsidian's cloud. Obsync is an alternative that keeps your notes on your devices:
42042.42042 and shows its fingerprint.http://10.174.223.140:42042).Auto-discovery never guesses: a host is accepted only if it answers the Obsync ping with a valid protocol message, so an unrelated service on the same port is ignored. The URL is normalized for you:
10.174.223.140:42042works, nohttp://needed. Seesrc/core/transport.ts(normalizeServerUrl) andsrc/core/discovery.ts.
One device (usually your laptop) runs the server; the other device (usually your phone) runs the client. The laptop is authoritative for deletions — the phone is additive-only, so a partially synced phone can never delete files on the server.
Laptop (authoritative) Phone (additive client)
┌──────────────────────────────────┐ ┌───────────────────────────────┐
│ Obsidian app │ │ Obsidian app │
│ └─ Obsync plugin │ │ └─ Obsync plugin │
│ ├─ RPC server :42042 │ │ └─ HTTP client │
│ └─ Sync engine │ │ └─ Sync engine │
│ └─ .obsync/index.json │ │ └─ .obsync/index.json│
│ └─ Vault folder │ │ └─ Vault folder │
└──────────────────────────────────┘ └───────────────────────────────┘
│ ▲ │ ▲
└────────────┴────────────────────┴────────────┘
HTTP /rpc (requestUrl)
The laptop runs an RPC server on port 42042; the phone (and the laptop's own
"Sync now") talk to it over HTTP POST /rpc — one protocol message per
request, the reply is the next message. Every message after the handshake
travels inside an encrypted envelope (see Security model).
Phone (client) Laptop (server)
│ 1. pair_request (device_id, fingerprint, name) │
│─────────────────────────────────────────────────────────►│
│ 2. pair_ack { approved: false } device recorded │
│◄─────────────────────────────────────────────────────────│ as pending
│ │
│ 3. Laptop user clicks "Approve" │
│ device → .obsync/approved.json │
│ │
│ 4. pair_request (same device) │
│─────────────────────────────────────────────────────────►│
│ 5. pair_ack { approved: true } │
│◄─────────────────────────────────────────────────────────│
│ 6. hello (device_id) │
│─────────────────────────────────────────────────────────►│
│ 7. hello_ack { approved: true } │
│◄─────────────────────────────────────────────────────────│
│ 8. full sync session follows │
Client (phone or laptop) Server (laptop)
│ 1. hello │
│────────────────────────────────────────────────────────►│
│ 2. refresh index (authoritative)
│◄────────────────────────────────────────────────────────│ 3. hello_ack { approved }
│ 4. manifest (client file states) │
│────────────────────────────────────────────────────────►│
│◄────────────────────────────────────────────────────────│ 5. manifest (server file states)
│ 6. diff → pull / push / delete │
│ 7. file_request (path, offset) │
│────────────────────────────────────────────────────────►│
│◄────────────────────────────────────────────────────────│ 8. file_chunk (base64, is_last)
│ 9. sync_operation (create / update / delete) │
│────────────────────────────────────────────────────────►│
│◄────────────────────────────────────────────────────────│ 10. operation_ack
│ 11. disconnect │
Step 6 is where the client decides what to do with each file: pull files the server has, push files only the client has, and apply tombstones. Every step that touches a single file is isolated — a chronically-conflicting file cannot stall the rest of the session.
File differs on both sides
│
▼
Did either side change since the last agreed sync hash?
│
┌───────┴────────┐
▼ ▼
"No" "Both changed"
│ │
▼ ▼
newer mtime wins CONFLICT
(no agreement, → Settings → Obsync → Conflicts
pre-v2 fallback) Keep local / Keep remote / Keep both
Conflict model: revisions are per-device counters and are not a reliable "both changed" signal — the real signal is the
synced_hashcolumn (the content hash the last sync agreed on). A conflict is flagged only when both sides changed since that agreement. Seesrc/core/conflict.ts.
Anyone on the same network can address the sync port, so Obsync treats the network as hostile and derives trust from cryptography instead:
.obsync/identity.json). The 8-byte SHA-256 prefix of the public
key is the fingerprint shown during pairing — approvals are bound to this
fingerprint, never to a self-claimed device id.hello carries the client's public
key and a fresh nonce, the server replies with its own nonce, and both sides
derive the same AES-256-GCM key via HKDF-SHA256. Every following message is a
sealed envelope {token, sequence, iv, ciphertext} with an ever-increasing
sequence number (replay protection) and the token+sequence bound as AEAD
associated data (tamper protection).ping (stateless discovery) is answered
without a session; everything else gets authentication required. An
unapproved key never receives a session, so it cannot read even the manifest.Everything lives under Settings → Obsync:
| Section | What it does |
|---|---|
| Device identity | Device name + fingerprint shown to the server during pairing |
| Sync server | Start/stop the RPC server on the authoritative device (port 42042) |
| Setup (mobile) | One-tap entry to the onboarding flow that auto-discovers the server |
| Connection (mobile) | Live status ("Connected" / "Server not found"), plus Test connection and Change server |
| Server address (advanced) (mobile) | Manual server URL (normalized); leave empty to auto-discover again |
| Auto-sync | Sync on vault changes and (on mobile) poll the server every interval |
| Sync now | Force a session on demand (also available via the ribbon icon and the command palette) |
| Sync selection | Everything, or only files/folders you pick (see below) |
| Excluded files | Files kept out of sync in either mode |
| Devices | Approved devices, pending approval requests, revoke access |
| Conflicts | Files changed on both sides — resolve per file: keep local / keep remote / keep both |
| Versions | Snapshot history of every file; restore any past version |
Two ways to control what syncs, under Settings → Obsync → Sync selection:
Both modes share the same rules: selection is a pure filter over the index, so picking and unpicking never touches file content, and re-including a file resumes from the last version both sides agreed on without manufacturing conflicts. Folders can also be scoped per device from the desktop dashboard's Sync scope card; a file exclusion always wins over any folder rule.
create/modify/delete events trigger a debounced
sync (~150 ms) on both platforms.Both are gated by a syncInProgress guard so sessions never overlap. See
main.ts.
npm install
npm run build # tsc typecheck + esbuild bundle → main.js
npm test # vitest (96 tests: engine, sync, store, scanner, crypto, pairing…)
The engine is a TypeScript port of the Rust sync engine in chintu79/obsync, with cross-language conformance tests keeping the two implementations equivalent.
| Path | What it is |
|---|---|
main.ts |
Plugin entry: server/client bootstrap, auto-sync, status bar |
src/core/engine.ts |
Port of the Rust sync engine (index, manifest, conflict planning) |
src/core/session.ts |
The sync session protocol (hello/manifest/pull/push) |
src/core/pairing.ts |
Device approval + pending-request tracking |
src/core/transport.ts |
HTTP framing + server + URL normalization |
src/core/store.ts |
JSON state store (.obsync/index.json) |
src/core/conflict.ts |
Divergence detection (the synced_hash rule) |
src/ui/settings-tab.ts |
Settings UI: server, URL, auto-sync, devices, conflicts, versions |
docs/device-test.md |
End-to-end checklist for testing on real hardware |
Releases are cut by maintainers. Tag a version with the exact
manifest.json version, no v prefix:
git tag 1.0.4
git push origin 1.0.4
The release workflow builds main.js and
attaches main.js + manifest.json + styles.css to a GitHub release.
We welcome contributions of all kinds — code, docs, bug reports, feature ideas. See CONTRIBUTING.md to get started.
httpd dashboard, and Android app this plugin's engine is ported from.Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Obsync shall be dual-licensed as above, without any additional terms or conditions.
© 2026 Obsync contributors.