
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.
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