Self Hosted Private Sync¶
Self-hosted, end-to-end encrypted live sync for Obsidian. Your notes sync through a server you run yourself. Notes, attachments and file names are encrypted on your device, and the server never receives the key. The plugin is built for every platform Obsidian runs on, desktop and mobile. There is no subscription and no account anywhere else.
Something not working? → Troubleshooting
Every page says who it is written for on its first line, and the navigation is grouped the same way.
Use obsync¶
For the person at the device: task first, what you see and what to do.
| I want to… | Go to |
|---|---|
| Choose how my devices reach my server | Choose your setup |
| Set up everything on my home network, with every phone screen | Same network, step by step |
| Install or update the plugin | Install the plugin |
| Set up my first device and pair the others | Quickstart |
| Know what the status bar and the commands mean | Daily use |
| Know what a setting does | Settings |
| Deal with a conflict copy | Conflicts |
| Fix a problem | Troubleshooting |
| Get back in after losing a device | Recovery |
Run a server¶
For the person operating it. Any host you control, any reverse proxy, VPN or tunnel you trust; Cloudflare is one optional choice.
| I want to… | Go to |
|---|---|
| Run it with Docker or Compose | Docker and Compose |
| Run it on Kubernetes | Kubernetes and the chart reference |
| Reach it away from home | Reaching it from outside your LAN |
| Use Cloudflare | Cloudflare (optional) |
| Back it up and upgrade it | Back up the two volumes and upgrade by digest |
| See its devices and storage | The dashboard |
| Understand its volumes and every storage refusal | Storage and durability |
| Wipe it and start again | Purging a server |
Trust and privacy¶
| I want to… | Go to |
|---|---|
| Know what the plugin touches | What this plugin accesses |
| Know what is encrypted and what the server can see | Threat model |
| Know what the dashboard defends | The dashboard's threat model |
| Report a vulnerability | Security policy |
Internals¶
For contributors and reviewers: Architecture, Protocol, Releases, CI map, the device validation plan and its runs, Benchmarks, Platform onboarding, Screenshot conventions and Translations.
This site is a rendering of the docs/ folder of
snaraj/obsync. Every page here is a
Markdown file in that repository, reviewed in a pull request like any other
file, and readable without this site. A few documents live at the root of the
repository rather than in docs/ — the README, the changelog, the security
policy, the contributing guide, the agent contract, and the Helm chart's own
README — and the navigation links to them where they are instead of keeping a
second copy here.
Where things live¶
| Path | Contents |
|---|---|
crates/obsync-core |
Homegrown primitives: SHA-256, HMAC, HKDF, CRC32, encodings, JSON, HTTP/1.1 |
crates/obsyncd |
The server: storage engine, journal, sync API, dashboard, CLI |
plugin/ |
The Obsidian plugin (TypeScript, WebCrypto, no runtime dependencies) |
dashboard/ |
Static dashboard assets, served by the server from OBSYNC_DASHBOARD_DIR |
chart/ |
Helm chart |
deploy/compose/ |
The Compose deployment: the server and a Caddy TLS front |
scripts/ |
CI gates, contract suites and validation probes |
docs/ |
This site |
Start with
AGENTS.md (the
contract) and Architecture.
Building this site¶
From a checkout, on any machine with a container runtime:
docker run --rm --platform linux/amd64 -v "$PWD:/repo" -w /repo \
python:3.12-slim sh -c 'pip install --require-hashes --no-deps \
--only-binary=:all: -r docs/requirements.txt && mkdocs build --strict \
&& python3 -B scripts/ci/site_origins.py strip site \
&& python3 -B scripts/ci/site_origins.py assert site'
The last two commands are the same ones
.github/workflows/docs-site.yml runs before it uploads anything, and they are
a pair. The vendored theme bundle carries loads to other hosts whatever
mkdocs.yml says -- two script injections and the addresses it builds to ask a
code-hosting API about this repository -- so strip rewrites the injections
into the no-op their own else branch already is and neutralises every other
address a script carries. assert then reads the OUTPUT and refuses anything
left: HTML through a parser (every src, srcset candidate, poster, data,
action, formaction, loading link and meta refresh, in any case and any
quoting, protocol-relative addresses included), stylesheets for url() and
@import, scripts for every string and template literal that carries an
address, and an output too thin to have been judged at all. A link a reader may
CLICK is a navigation rather than a load: those are counted and the count is
printed. Requirement 1 admits no third-party runtime dependency, and a setting
in mkdocs.yml is not evidence about the bytes a reader downloads.
The container is not decoration. docs/requirements.txt pins every package by
exact version AND by the sha256 of the exact wheel, and a wheel's sha256 is a
fact about ONE file: the closure is resolved and hashed for CPython 3.12 on
linux/amd64, the interpreter and platform
.github/workflows/docs-site.yml pins. --require-hashes therefore refuses a
venv on another interpreter — and so, before it, does --only-binary=:all:,
because several of these packages publish no wheel at all for a newer Python.
Running the pinned image is how a reader gets the bytes CI gets.
mkdocs.yml at the repository root carries the navigation and the reasoning
behind it. .github/workflows/docs-site.yml runs the same install and the same
mkdocs build --strict on every pull request, and deploys the result to GitHub
Pages on pushes to main.