← All posts

Stop renting your second brain

#obsidian#couchdb#homelab#self-hosted#sync

Obsidian Sync is a few bucks a month. iCloud sync exists if you squint. Notion will happily host your second brain for the rest of your life, billed monthly.

I'm not going to argue any of that is a bad deal in dollars. The deal is bad on a different axis: you don't own the sync, and you don't own the protocol.

When your notes app's sync server gets sold, raises prices, changes its terms, or quietly starts training models on your journal entries, your options are: pay, leave, or hope. None of those is "keep going."

I host my Obsidian vault sync on a small LXC in my Proxmox lab. It costs me electricity. More importantly, the thing on the other end of my notes is a database I can dump, replicate, migrate, and reason about. Here's how it works. And more importantly, why the right primitive matters.

The wrong instinct: "I'll just put it behind a REST API"

If you handed a backend engineer this problem cold ("sync markdown files across three devices, two of which are often offline"), most would reach for Postgres plus a REST API. Maybe S3 with a manifest. It would look reasonable in a system design interview.

It would also be wrong.

Sync is not CRUD. The minute you have multiple writers, often offline, occasionally on flaky networks, you are not building an API. You are building a replication problem. Replication problems have known-correct primitives. REST plus last-write-wins is not one of them.

Three specific things break the REST instinct:

1. Concurrent edits. My laptop and my phone both edit the same note while I'm on a plane. Both come online an hour later. What does your API do? If your answer involves a timestamp, you're losing data silently. The correct answer involves a revision tree: both edits exist, and the conflict is explicit and resolvable.

2. Clients are not caches. In a REST world, the server is the source of truth and clients are dumb. In sync, every client is a full replica of the database. That's the whole point of local-first: your laptop must be useful offline, with the same data shape as the server. PouchDB, the client library Obsidian's LiveSync plugin uses, is structurally a CouchDB.

3. The protocol matters more than the implementation. CouchDB's replication protocol is documented and open. PouchDB to CouchDB to another CouchDB to a Cloudant instance. All interoperable. Your notes are not trapped behind anyone's proprietary API. That's an architectural property, not a feature you bolt on later.

This is why CouchDB is the right primitive here. It's not the only one. Automerge, Yjs, and friends play in similar territory for CRDT-based approaches. But for a markdown vault (coarse-grained docs, occasional conflicts, no real-time collaboration), CouchDB hits the sweet spot.

What actually happens when I save a note

Concretely, on every edit:

  1. Obsidian writes the file to disk locally. Always. Offline-first, no network in the path.
  2. The LiveSync plugin (PouchDB under the hood) writes a new revision to its local IndexedDB.
  3. PouchDB replicates that revision to my CouchDB over HTTP. If I'm offline, it queues.
  4. CouchDB stores the new revision and assigns it a _rev ID in the revision tree.
  5. Every other device on the tailnet pulls the new revision on its next sync cycle.
  6. Conflict? Both revisions stay in the tree. The plugin surfaces the conflict and I resolve it in Obsidian.

No edit is ever lost in transit. No edit is ever overwritten silently. That's the guarantee, and it's a guarantee REST plus Postgres won't give you without a lot of bespoke code that you will get wrong.

The actual setup

Not exotic. The interesting bits are the parts most generic CouchDB tutorials miss.

LXC, not VM

Proxmox LXC is plenty. CouchDB idles at a few hundred MB. One container, Debian 12, static IP on the lab subnet.

Two flags you must enable before first boot, or Docker will fail in confusing ways:

  • Nesting, required for Docker inside LXC.
  • keyctl, required for newer container runtimes.

This is the number one thing that bites people. The error messages don't point at it.

The compose stack

services:
  couchdb:
    container_name: obsidian-livesync
    image: couchdb:3.3.3
    environment:
      - TZ=America/Santiago
      - COUCHDB_USER=obsidian_user
      - COUCHDB_PASSWORD=__use_a_real_password__
    volumes:
      - ./data:/opt/couchdb/data
      - ./etc:/opt/couchdb/etc/local.d
    ports:
      - "5984:5984"
    restart: unless-stopped

Two volumes: one for data, one for config overrides. Mounting local.d is what lets you bake config as files instead of poking Fauxton every time you rebuild.

The config Obsidian needs (that nobody documents well)

CouchDB's defaults don't speak Obsidian. Drop this into ./etc/10-livesync.ini:

[chttpd]
require_valid_user = true
enable_cors = true
max_http_request_size = 4294967296

[chttpd_auth]
require_valid_user = true

[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true

[couchdb]
max_document_size = 50000000

[cors]
credentials = true
origins = app://obsidian.md,capacitor://localhost,http://localhost

The non-obvious ones:

  • max_http_request_size and max_document_size. Without these bumped up, large attachments fail silently on first sync of a real vault. You'll think it works, then realize half your PDFs didn't make it.
  • cors.origins. Obsidian desktop, mobile (Capacitor), and dev all use different origins. Miss one and the plugin returns cryptic auth errors.
  • require_valid_user = true in both chttpd and chttpd_auth. Yes, both. Setting one isn't enough.

Don't expose this to the internet

Port 5984 should never see the public internet. Two patterns work:

  • Tailscale (what I do): every device that needs sync joins the tailnet, MagicDNS gives the LXC a stable name, the firewall sees nothing. Zero-trust, no port forwarding, works on mobile.
  • Cloudflare Tunnel with Zero Trust Access, if you need browser access from arbitrary devices. Slightly more setup, identity-based auth in front of CouchDB.

LiveSync also supports end-to-end encryption at the plugin level. Turn it on. Your CouchDB now stores ciphertext; even if someone walks off with the LXC, they get gibberish without the passphrase. Defense in depth, and it's free.

Backup

The ./data directory is your entire vault history. Snapshot the LXC nightly in Proxmox, rsync ./data to a separate disk weekly. You will mess up a sync conflict eventually and want to roll back.

What this actually buys you

  • No subscription. Not the point, but real.
  • No vendor. Obsidian the editor could disappear tomorrow and my notes are still markdown files on disk, with full revision history in CouchDB.
  • Portability that's actually a protocol. I can replicate my CouchDB to another CouchDB anywhere (another homelab, a VPS, a friend's box) with one API call. That's not a migration script. That's the replication protocol doing its job.
  • An actual reason to run a homelab. Self-hosting things you genuinely use beats running things "to learn." Skin in the game changes what you bother to debug at 11pm.

Pushback I expect

"Just use Syncthing."

Fair. Syncthing syncs files; CouchDB syncs documents with revision history and explicit conflict resolution. For markdown that rarely conflicts, Syncthing works. For a vault where you edit the same daily note from three devices, you'll hit silent conflicts. Different tools, different guarantees.

"Just use Git."

Also fair, and what I did for a year. Git is great until you want sync to be transparent on mobile, or you want a non-engineer to use the same vault without learning rebases.

"This is overkill for notes."

Probably. It's also a working production-grade sync engine running on hardware I own, which is a better $0 weekend than most tutorials.

Your turn

If you self-host your notes, what's your stack? Syncthing? Git? Something weirder? And if you don't, what's the thing keeping you on a hosted sync? Genuinely curious.

And if you want to argue I should be using a CRDT-native store instead of CouchDB, I'm extremely here for that fight.