Syncing One Obsidian Vault Across Android, a Laptop, and a Home Server
I wanted the same Obsidian notes across three devices: a laptop where I edit in VSCodium, a browser editor on a home server, and an Android phone. The laptop setup already worked. The phone was the awkward part.
Adding Syncthing to the phone looked like the shortest route. It was also the wrong one. The Obsidian plugin and Syncthing would both be watching the same mobile folder. One edit could become two competing updates, then a conflict file, then a second cleanup job.
I kept Syncthing on the laptop and gave Android a separate route through CouchDB. The server folder stayed where the browser editor expected it.
The layout
- Laptop: VSCodium edits a local vault folder. Syncthing sends those file changes to the home server.
- Browser editor: WebObsidian reads and writes the real vault folder on that server.
- Android: Obsidian Mobile keeps its local copy for offline work. The Self-hosted LiveSync plugin replicates it with CouchDB over a private HTTPS route.
- Home server:
livesync-cliruns as a daemon and mirrors the CouchDB database with the same server-side vault folder.
CouchDB is the phone's sync hub. It does not replace the Markdown folder. The folder remains the shared working copy for the browser editor and laptop.
Syncthing keeps the laptop-to-server lane. LiveSync connects Android to CouchDB, and the daemon joins CouchDB to the server folder. Do not add a second sync engine to the same Android vault directory.
Decide what belongs on the phone first
A vault can hold active notes, old exports, scans, videos, and other files that are useful on a desktop but unnecessary on a phone. If the entire archive is copied into CouchDB, the phone will fetch it too. That means more storage, more network traffic, more database revisions, and more private material on a mobile device.
I kept the root archive/ folder outside mobile sync. I also left hidden files and device-specific Obsidian settings out. The exact ignore syntax belongs to the pinned LiveSync CLI release, so read that release's IgnoreRules.ts instead of assuming every pattern follows Git's .gitignore rules. In the CLI source used for this setup, a path pattern such as archive/** applies to the root archive path; a trailing-slash directory pattern can match directories at any depth. Negation patterns are not supported there.
A small example looks like this:
archive/**
.obsidian/
.livesync/
.stfolder
.stignore
.stversions/
*.sync-conflict-*
The rule for archive/ is a data decision, not a cosmetic filter. A note on the phone can still contain a link to a file under an excluded folder, but that image or attachment will not be available on the phone. If only a few archived images are needed, copy those files into a synced folder such as attachments/mobile/ and update the links. Do not enable the whole archive just to retrieve one picture.
For new attachments created on Android, Obsidian's Files & Links settings can place them in a folder inside the vault, for example attachments/mobile/. This keeps the vault root tidy while still allowing LiveSync to transfer the files. A path outside the vault is not an attachment location that LiveSync can manage. This setting affects new attachments only; it does not move existing files or new notes.
Before widening the sync scope, inventory file sizes and filenames that may represent credentials or personal records. Synchronization is not a backup, and an Android copy expands the number of places where data lives.
Put CouchDB behind a private HTTPS route
Install CouchDB on the home server with persistent data storage and a strong admin credential. Publish its port only on loopback, then expose it to the phone through a private VPN's HTTPS proxy. For Tailscale, the tailscale serve command provides the private HTTPS endpoint; see the Tailscale Serve reference.
A Compose skeleton should resemble this. It is illustrative, not a drop-in production file. Pin the image and add the resource limits, permissions, backup paths, and host-specific protections required by your server.
services:
couchdb:
image: couchdb:<pinned-version-or-digest>
ports:
- "127.0.0.1:5984:5984"
environment:
COUCHDB_USER: "${COUCHDB_USER:?required}"
COUCHDB_PASSWORD: "${COUCHDB_PASSWORD:?required}"
volumes:
- ./couchdb-data:/opt/couchdb/data
- ./couchdb-etc:/opt/couchdb/etc/local.d
Mount the configuration directory writable. Some CouchDB image entrypoints modify a file in that directory when they provision the admin account. A read-only bind of one config file can turn a normal first boot into a restart loop.
For a single-node server, the relevant settings include single-node mode, authenticated access, and CORS for the Obsidian mobile origins. Keep the values in a protected config file and check the CouchDB Docker guide and HTTP/CORS configuration reference for the version you deploy.
A minimal example of the settings used in this setup is below. Review the limits and allowed origins against the plugin release you actually install; do not paste this over an existing CouchDB config without checking it first.
[couchdb]
single_node = true
max_document_size = 50000000
[chttpd]
require_valid_user = true
enable_cors = true
max_http_request_size = 52428800
[chttpd_auth]
require_valid_user = true
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
[cors]
origins = app://obsidian.md, capacitor://localhost, http://localhost
credentials = true
methods = GET,PUT,POST,HEAD,DELETE
headers = accept, authorization, content-type, origin, referer
The mobile plugin sends browser-style CORS requests. CORS does not authenticate a user. Keep CouchDB authentication enabled, verify an unauthenticated request gets 401, and verify an authenticated request succeeds. Test the CORS preflight through the private HTTPS hostname from another tailnet device. A self-request from the server to its own VPN hostname can fail because of hairpin routing even when a laptop or phone can reach it.
Create the database through CouchDB's HTTP API. Treat 201 Created and 412 Precondition Failed as the normal create-or-already-exists outcomes. Keep the actual password out of logs, shell history, screenshots, and chat.
Bridge the database to the real vault folder
Build the headless CLI from an exact upstream tag or commit. Do not build from latest and assume the setup flags stay stable. The LiveSync CLI README documents its database path, vault path, daemon, mirror, and ls commands.
The database path and the Markdown vault path are different things. The database directory holds local database state and settings. The vault path is the actual folder served by the browser editor and watched by Syncthing. A Compose service can make that split explicit:
services:
livesync:
image: livesync-cli:<same-pinned-release>
environment:
LIVESYNC_DB_PATH: /data
volumes:
- ./cli-db:/data
- /path/to/canonical-vault:/vault:rw
command: ["daemon", "-V", "/vault"]
Match the container UID/GID to the owner of the mounted vault. Add read-only root filesystem, dropped capabilities, resource limits, and other hardening where the image permits it. Do not publish a port for the CLI container.
Run daemon as the long-lived service. mirror is a one-shot scan that exits. The initial daemon scan also does not infer every deletion from a missing file; after a container restart, a file deleted during the gap can reappear in the folder. The LiveSync CLI README describes this behavior and the differences between a file path and the local database path.
Bootstrap the local settings and remote connection once. Verify that the settings are marked configured and the selected database name matches the CouchDB database before starting the long-lived service. Connection-string syntax has changed across implementations; follow the parser and docs for the exact release you pinned. In the CLI release used for this setup, the database name was supplied as a db query parameter. Do not assume a URL path and a query parameter mean the same thing.
After creating .livesync/ignore, restart the daemon so it reloads the rules. Wait for the remote document count to settle over more than one observation, but do not compare that count directly with the number of files. LiveSync splits a large file into multiple chunk documents.
If you need a file-list probe against the CLI's local database, stop the daemon first. Running the CLI against the same local database while the daemon has it open can hit a LevelDB lock. Restart the service after the probe, even when the probe fails.
Connect Android without reusing the old Syncthing copy
On the Android device, remove only the old vault folder from the Syncthing app. Leave the laptop and server peers alone. Keep the old local copy until the new setup passes its tests; it is a fallback, not the new LiveSync vault.
Create a new, empty Obsidian vault on the phone. Install the Self-hosted LiveSync community plugin, then configure the database endpoint as an HTTPS URL reachable only inside the private tailnet, for example https://<server>.<tailnet>.ts.net/<database>.
Choose overwrite local files with remote files only when the phone vault is genuinely empty. If there are local-only notes worth keeping, stop and back them up or merge them deliberately first. When the plugin finds multiple saved remote profiles, verify the active connection instead of repeating the wizard and creating more profiles.
Then open Settings → Community plugins → Self-hosted LiveSync → Sync Settings. Select the LiveSync preset and tap Apply. Reopen the settings and make sure Sync Mode actually reads LiveSync. Selecting a preset in a menu without applying it leaves the old mode in place.
This was the failure that made my first setup look finished while it was not. The initial download completed. A manual Sync now also moved files. But the selected mode was still On events, so automatic remote updates did not behave like a continuous LiveSync connection. Applying the LiveSync preset fixed the mode. A one-shot sync button is a poor test for continuous synchronization.
For the plugin's own-server instructions, start with the Self-hosted LiveSync setup guide. Keep the plugin's end-to-end encryption setting as an explicit security decision. HTTPS protects traffic in transit; CouchDB authentication limits access; neither replaces end-to-end encryption. If E2EE is disabled, the server can read the notes.
Verify the whole path, not just the green icon
Use disposable canary files with unique names. Do not test by repeatedly editing a valuable note.
- Create a small note on Android. Confirm the file appears in the server vault folder, then check it from the laptop or browser editor.
- Create a second note on the server folder. Confirm the Android vault receives it without pressing Sync now.
- Edit an existing test note in the browser editor. Wait for the server file to change, then confirm the phone receives the same content.
- Create and edit a note while Android is offline. Reconnect and verify both sides converge without losing text.
- Delete a disposable note from the phone and check that its server file disappears.
- Close Obsidian, change a server note, reopen Obsidian, and verify catch-up.
- Restart CouchDB and check that the daemon and phone reconnect.
- Change networks and confirm the phone reconnects before relying on it away from home.
- Open the phone's file list and confirm excluded folders are absent by design.
- Compare hashes for a test file after conflict resolution. Do not assume a completed fetch means two live copies are identical.
A useful test result has a source, a destination, a unique marker, and a time window. “The sync icon looked fine” is not enough. Record which hop failed: phone to CouchDB, CouchDB to the server folder, or server folder to the browser/laptop.
Backups and conflict policy
Syncthing and LiveSync replicate changes. If a bad edit or delete reaches every peer, synchronization can spread the mistake too. Keep an independent, off-host backup and test restoring it before trusting the setup with irreplaceable records.
Avoid simultaneous edits to the same file from the phone, browser editor, and laptop. LiveSync keeps conflicting revisions, and the plugin can sometimes merge text. That is a recovery aid, not a reason to invite conflicts. After a merge, compare the resulting file with the intended content.
The setting that made automatic sync work
The first fetch had completed, and pressing Sync now moved files. Automatic updates still did not arrive. The phone was left in On events mode because I had selected the LiveSync preset but had not tapped Apply.
After applying the preset, I created one canary on the phone and one on the server. Both appeared on the other side without pressing Sync now. That is the check I would repeat on a new phone before trusting the setup.
Sources
- Self-hosted LiveSync project
- CouchDB setup guide for the LiveSync plugin
- LiveSync CLI reference
- LiveSync CLI ignore-rule implementation
- CouchDB Docker installation
- CouchDB HTTP and CORS settings
- Tailscale Serve CLI
- Syncthing conflict-file FAQ
- Obsidian attachment settings
AI assistance: GPT-6 Luna (OpenAI Codex) and GLM-5.3 Flash (Z.ai) assisted in preparing this article. The author is responsible for the final published version.