This blog post was written with Obsidian

This blog post was written with Obsidian

I migrated my blog posts from being committed in GitHub to a fully self-hosted Obsidian LiveSync + CouchDB setup, and built a FastAPI app to serve them directly.

3 min
Published September 14, 2025
FastAPI
Obsidian
CouchDB
Proxmox
Python
Homelab
Self-hosting

For a long time, I stored my blog posts as markdown files directly in my Next.js repo.

Every small edit to a post meant committing non-code into git, which polluted my commit history. Worse, my setup had a RAG pipeline with OpenAI + AstraDB + Upstash, and each rebuild flushed the database and re-generated embeddings. Editing content felt heavier than it should.

Inspiration

This whole idea started when I came across a post on r/selfhosted. Someone described syncing Obsidian with CouchDB using the Obsidian LiveSync plugin, and it clicked immediately.

I'm into self-hosting and homelab tinkering, so moving to Obsidian with LiveSync was appealing. I could keep all my notes in Obsidian, sync them automatically into a CouchDB LXC container on Proxmox, and separate code from content.

  • Open source & free – no vendor lock-in
  • Instant sync – edits in Obsidian reflect right away
  • Cleaner repos – no more commits for every typo

It finally felt like the "correct" way to manage blog posts.

The CouchDB Surprise

At first, I was confused – CouchDB didn't store my notes as simple documents. Instead, each markdown file was split into chunks of children documents, with the main .md doc only referencing its child IDs.

Coming from Postgres, this was unexpected. I had to poke around in /_utils to figure out how the pieces fit. Eventually, I wrote a ContentParser class in Python to reassemble posts by walking the child docs and joining their data fields.

This step was crucial – without it, all I saw were empty posts.

Serving Posts with FastAPI

Once I could reconstruct the markdown, the next step was exposing it. I built a small FastAPI app that:

  • Lists all posts (/posts) with metadata parsed from frontmatter (title, summary, tags, etc.)
  • Serves a full post by slug (/posts/{slug}) with both metadata and content

That gave me the same data my Next.js frontend expected, without embedding the markdown files in the repo.

Handling Images

The next annoyance was images. My blog posts used a mix of relative and Obsidian-style links, which didn't line up neatly with Next.js' public/ folder.

To avoid duplication, I extended the FastAPI app to serve images directly from CouchDB. In practice this means:

  • No need to copy images into public/
  • Links stay consistent between Obsidian and the web
  • My posts now have something close to semi-permalinks for images
  • I don't need to rely on an external image hosting service

It's a small thing, but it makes the whole setup feel much more "mine".

Lessons so far

This setup already feels much cleaner. Content lives where it should (Obsidian), sync is automatic (CouchDB LiveSync), and serving posts is just another service in my homelab.

It was also a good exercise in exploring CouchDB's document model and learning how to bridge it into something my frontend can consume.


Next, I'll be extending this setup with more Obsidian content (resumes, personal notes, etc.) and wiring it into my embedding pipeline – but that's for another post.

-- Ted