Engineering

From Convex CMS to MDX: Why We Ditched Our Own Backend for Blog Content

We had a perfectly good real-time database. We used it to store blog posts. Then we came to our senses and migrated everything to MDX files you can actually review in a PR.

By Lumo EngineeringFeb 6, 20263 min read

I built a CMS in Convex. It worked. You could create posts, edit them in real-time, and they'd show up on the website instantly. Reactive queries, live preview, the works. It was genuinely cool engineering.

It was also completely wrong for the job.

The Problem with a Real-Time CMS for Static Content

Here's the thing about blog posts: they don't change in real-time. Nobody is sitting there refreshing your privacy policy hoping for a plot twist. Legal documents update maybe twice a year. Blog posts get published and then sit there, quietly accumulating dust and backlinks.

But we had them in Convex, which meant:

  • Every page load was a database query. The privacy policy — a document that changes approximately never — was hitting our backend on every single visit. That's compute time, bandwidth, and latency we were paying for no reason.
  • No version control. Want to see what the Terms of Service looked like three months ago? Good luck grepping through Convex's dashboard. Meanwhile, git log --follow content/legal/terms.mdx gives you every revision with full context and blame.
  • No PR review for content. Legal copy changes were mutations in a dashboard. No diff, no approval flow, no "hey, did you actually read what we're publishing?" Just vibes and a publish button.
  • Content was trapped. If we ever wanted to switch backends (unlikely with Convex, but still), our blog posts were locked in a proprietary database. MDX files are just text. They'll outlive every framework we'll ever use.

The Migration

PR #1 was deceptively simple: move everything to MDX files and delete the CMS code.

The new structure:

apps/web/content/
  blog/
    welcome-to-lumo.mdx
    building-lumo-pro-usage-based-tiers.mdx
    ...
  legal/
    privacy-policy.mdx
    terms-and-conditions.mdx

Each MDX file has frontmatter for metadata:

---
title: "Why I Built Lumo"
description: "The origin story nobody asked for"
author: "Lumo Team"
publishedAt: "2026-02-07"
tags:
  - product
  - insights
draft: false
---

The rendering pipeline uses next-mdx-remote to compile MDX at build time. Static generation means the content is pre-rendered HTML — zero database queries, instant load, perfect SEO.

The Monorepo Setup

This was also when we set up the Lumo monorepo properly. Bun + Turborepo with three workspaces:

apps/
  web/          # Next.js marketing site + blog
  mobile/       # React Native app
  admin/        # Admin dashboard (added later)
packages/
  backend/      # Convex — shared by all apps

Unified TypeScript configuration across everything. One bun install at the root, one turbo build to build the world. The kind of developer experience that makes you wonder why you ever tolerated separate repos with diverging configs.

What We Kept in Convex

To be clear — Convex is still our entire backend for everything that actually needs a database: users, transactions, budgets, subscriptions, real-time sync. We just stopped abusing it as a CMS for static text files.

The rule is simple now: if it changes when a user interacts with the app, it lives in Convex. If it changes when a developer writes prose, it lives in MDX.

The Boring Lesson

The boring lesson is that the best tool for the job isn't always the most technically interesting one. Convex is incredible for real-time data. It's overkill for blog posts. MDX files in a git repo are boring technology, and boring technology is what you want for content that needs to be reliable, reviewable, and permanent.

Sometimes the best migration is the one that makes your system dumber.


Abdul Rafay Founder, Syntax Lab Technology

Loading...