Jiří Čepelka 225 downloadsSync your vaults through your own self-hosted SimpleSync server. Enter the server address, name and password, then pick a vault.
Simple self-hosted sync for Obsidian: a small server plus an Obsidian plugin. No CouchDB, no S3 keys, no config files to tune.
In Obsidian you enter server address, name and password, pick a vault from the list, and you're done.
/p/<name>/.… (conflict …) is kept.Open App Store → "+" → Install a customized app, switch to YAML, delete what's there (Ctrl+A, Delete) and paste this (also in deploy/zimaos/docker-compose.yml):
name: obsisync
services:
obsisync:
image: ghcr.io/jirkacepelka/obsisync:latest
container_name: obsisync
restart: unless-stopped
ports:
- target: 8080
published: "8080"
protocol: tcp
environment:
TZ: Europe/Prague
volumes:
- type: bind
source: /DATA/AppData/obsisync/data
target: /data
x-casaos:
architectures:
- amd64
- arm64
main: obsisync
category: Utilities
title:
en_us: SimpleSync
tagline:
en_us: Simple self-hosted sync for Obsidian
icon: https://raw.githubusercontent.com/jirkacepelka/SimpleSync/main/server/internal/web/static/icon.svg
index: /
port_map: "8080"
scheme: http
Click Install, then open http://<zimaos-ip>:8080.
Create the administrator account (first-start wizard).
Data lives in /DATA/AppData/obsisync/data.
Access from outside your home network (a phone on mobile data): the easiest option is Tailscale (available in the ZimaOS App Store) or a Cloudflare Tunnel. HTTPS is recommended for iOS; Tailscale provides free HTTPS certificates (
tailscale serve).
A 1 GB RAM VPS with Docker is enough.
# 1) DNS: an A record sync.example.com → the VPS IP address
# 2) on the VPS:
mkdir obsisync && cd obsisync
curl -O https://raw.githubusercontent.com/jirkacepelka/SimpleSync/main/deploy/docker-compose.caddy.yml
DOMAIN=sync.example.com docker compose -f docker-compose.caddy.yml up -d
Open https://sync.example.com and create the administrator. Caddy obtains the Let's Encrypt certificate by itself.
docker run -d --name obsisync -p 8080:8080 -e TZ=Europe/Prague \
-v $PWD/data:/data --restart unless-stopped ghcr.io/jirkacepelka/obsisync:latest
Pull the new image and recreate the container (on ZimaOS: the app's settings → update / reinstall). Your data in /data is kept.
In the web admin click Download for Obsidian next to a vault (or Plugin → Download ready-made vault for an empty one). You get a ZIP with a folder that contains the vault's notes and the SimpleSync plugin, already installed and set up for your server and name (no password or token is ever put in the ZIP).
<vault>/.obsidian/plugins/ (a simplesync folder appears) and enable Settings → Community plugins → SimpleSync. On a phone, the easiest way is the BRAT plugin with the repository jirkacepelka/SimpleSync.The status bar icon shows the state: ✓ synced, ⟳ syncing, ⚡ server unreachable, ⚠ error. Click it to sync right away. The plugin language follows Obsidian; you can change it in the plugin settings.
⚠️ Connecting to a server vault that already has files replaces the content of this Obsidian vault. Nothing is uploaded from this device; local files that differ are moved to Obsidian's trash (
.trash). The plugin shows a warning in its settings and asks before connecting. If you want to upload an existing vault, use "Create a new vault from this one" instead.
.trash), so nothing is lost. An empty or brand-new Obsidian vault can never overwrite the server.After the first sync, both directions sync normally.
Open a vault in the web admin and click Open notes (or the Notes tab). You get a file tree, an editor and a live preview side by side, so you can write from any browser without installing Obsidian.
name (web).… (conflict …).md copy, exactly like on a device.[[wikilinks]] (with |alias and #heading), embedded images ![[photo.png]], callouts > [!tip], ==highlights==, #tags, task lists, tables and properties (front matter).[[ suggests notes to link. The toolbar and shortcuts (Ctrl+B/I/K) cover the usual formatting; lists continue on Enter.A vault owner can publish notes as a small public website: Vault → Settings → Publishing.
https://<your server>/p/<address>/.publish: true in their properties; the Publish button in the editor toggles it) or a whole folder (empty = the whole vault).Only published notes are reachable. Links to notes that aren't published are shown as plain text, so their names and paths don't leak, and an image or file is served only when a published note links to or embeds it. Visitors need no account. Raw HTML in notes is never rendered, and public pages send a strict Content-Security-Policy.
The server speaks the Model Context Protocol at https://<your server>/mcp, so an AI agent can work with your notes directly, without a synced local folder: answer questions from your notes, file a daily summary, clean up an inbox, or run on a schedule (for example a scheduled task in Claude). Everything it changes goes through the same sync as the plugin: it reaches your devices within seconds, is kept in the version history as name (agent), and deleted notes land in the vault's Trash.
Authorization: Bearer <token>, e.g. claude mcp add --transport http simplesync https://<your server>/mcp --header "Authorization: Bearer osa_…".Tools: list_vaults, list_notes, read_note, search_notes, recent_changes, and with write access create_note, update_note (merges with edits made meanwhile when given the hash from read_note), append_to_note, move_note, delete_note. Obsidian's own files (.obsidian/…) are hidden, and only text files can be written.
A token (or a Claude connection) belongs to one user and never gets more than that user's role: a read-only member stays read-only whatever the token says. Every connection is listed on the AI agents page with when it was last used, and revoking it cuts it off immediately.
| Section | What it does |
|---|---|
| Overview | vaults, disk usage, devices online, alerts about failed backups |
| Vaults | create a vault (name, backup frequency, how long to keep backups, members) |
| → Notes | the web editor (see above) |
| → Files | browse folders, preview notes and images, version history with restore, download the whole vault as ZIP |
| → Trash | deleted files and restoring them |
| → Backups | list of backups, Back up now, ZIP download, restore a single file or the whole vault |
| → Members | share the vault with other users: Owner / Editor / Read only; with folder access on, their roles and folders of their own |
| → Roles | roles that give folders for reading or editing (only when folder access is on, see below) |
| → Settings | rename, change the backup plan, publishing, folder access, delete the vault |
| Users | create accounts, reset passwords, administrators |
| Devices | every Obsidian login; logging a device out removes its access immediately |
| AI agents | tokens and Claude connections for the MCP endpoint (see above) |
| Settings | how long to keep version history, maximum file size, whether users may create vaults |
The language picker is at the bottom of every page.
By default a member sees the whole vault. When people should see only part of it, the vault owner switches on Folder access in the vault's Settings. It is off by default and changes nothing until switched on; switching it off makes the whole vault visible to every member again (roles are kept for later).
With folder access on, members who are not owners see nothing until they are given folders:
Education → Edit, Shared → Read. Whole vault can be picked as a folder too.The server enforces this everywhere: other files do not exist for that member in Obsidian, the web editor, file history, trash, ZIP downloads, links, search or AI agents, and changes outside folders given for editing are refused. Members limited this way do not see Backups, because a backup is a snapshot of the whole vault. When a folder is taken away, the Obsidian plugin moves its files to the Obsidian trash (files the member had edited are kept, but no longer synced); when a folder is given, it arrives with the next sync. Edits in a folder given for reading stay on that device and the status bar says how many were not uploaded.
Folders are given by path. Renaming a folder in the web editor keeps its access, but a rename in Obsidian does not (the server only sees files being deleted and created), so the Roles and Members tabs mark folders that no longer exist. The .obsidian settings folder is a folder like any other: give it for reading if those members should get the shared Obsidian settings. Members need the Obsidian plugin 0.8.0 or newer.
When creating a vault, the administrator chooses:
data/backups/, handy for copying to another diskHow it works:
Everything (database, file contents, ZIP backups) is in the data/ folder. Back up that folder. For a consistent copy of the database while running:
docker exec obsisync obsisync backup-db /data/obsisync-backup.db
docker exec obsisync obsisync reset-password admin NewPassword123
(If the user doesn't exist, it is created as an administrator.)
The SimpleSync plugin communicates only with the SimpleSync server whose address you enter, a server you run yourself. It sends your login once to obtain a device token (the password is not stored), then uploads and downloads the files of the connected vault. To do that it lists all files in the vault and compares them with the server; nothing is sent anywhere else. There is no telemetry, no third-party service and no account with anyone else. Content is protected in transit by HTTPS when the server is reachable over HTTPS; it is not end-to-end encrypted, so whoever runs the server can read the notes stored on it.
X-Forwarded-For. Devices and browsers that are already logged in keep working while an account is throttled.HttpOnly and SameSite=Lax, every form has a CSRF token, pages send a strict Content-Security-Policy and may not be framed, and files from vaults are only ever downloaded (never rendered as HTML/SVG)..obsidian are only synced when you turn that on; only do so in vaults shared with people you trust, because synced plugins run on every device.http:// outside your home network.X-Forwarded-For only when the connection comes from a local or private address, and then only the entry added by the nearest proxy.Obsidian (desktop / mobile) Server (1 Docker container)
┌───────────────────────┐ HTTPS (REST) ┌────────────────────────────────┐
│ SimpleSync plugin │◄────────────────►│ SimpleSync server (Go) │
│ • 3 fields + picker │ WebSocket │ • /api/v1 sync │
│ • sync engine │◄──────────────────│ • / web admin │
│ • 3-way merge │ ("new revision") │ • SQLite metadata, history │
└───────────────────────┘ │ • blobs/ content (SHA-256) │
│ • backup scheduler + upkeep │
└────────────────────────────────┘
How sync works
Note (conflict 2026-09-25 1530 jirka).md (with the name of the user whose edit it is),.trash/, .git/, other hidden folders, workspace.json and the plugin's own data (token). The .obsidian folder can be enabled with a toggle, and individual plugins can be excluded from it.API: everything is under /api/v1, authorized with Authorization: Bearer <device token>:
| Endpoint | Purpose |
|---|---|
POST /auth/login |
name + password → device token (the password is not stored on the device) |
GET /vaults, POST /vaults |
the user's vaults / create one |
GET /vaults/{id}/changes?since=REV |
changes since a revision |
POST /vaults/{id}/blobs/missing |
which content the server doesn't have yet |
PUT / GET /vaults/{id}/blobs/{sha256} |
upload / download content |
POST /vaults/{id}/commit |
a batch of changes (compare-and-swap) |
GET /vaults/{id}/ws |
WebSocket notifications |
server/ Go server (cmd/obsisync, internal/{store,api,web,backup,blobs,auth,hub,i18n,markdown,textmerge})
plugin/ Obsidian plugin (TypeScript); src/engine is Obsidian-independent and tested
deploy/ docker-compose for home network, VPS with Caddy, and ZimaOS
Dockerfile multi-arch image (amd64 + arm64) with the server and the plugin
# server
cd server && go test ./... && go run ./cmd/obsisync # http://localhost:8080, data in ./data
# plugin (tests start a real server and simulate several devices)
cd plugin && npm ci && npm test && npm run build # output in plugin/dist
Translations: the web admin uses server/internal/i18n/locales/<lang>.json, the plugin uses plugin/src/i18n/<lang>.ts. English is the source; missing keys fall back to English.
Releases: every push to main makes GitHub Actions build the Docker image ghcr.io/jirkacepelka/obsisync (latest and the version from manifest.json) and a GitHub release with the plugin files. To publish a new version, bump version in manifest.json, plugin/manifest.json and versions.json.
| Variable | Default | Meaning |
|---|---|---|
TZ |
UTC | time zone for displayed times (e.g. Europe/Prague) |
OBSISYNC_DATA |
/data |
data folder |
OBSISYNC_ADDR |
:8080 |
listen address and port |
OBSISYNC_BACKUP_DIR |
$OBSISYNC_DATA/backups |
where ZIP backups go |