I’m building OCR Trips — an obstacle course race trip planner that helps you:
- Discover races
- Plan trips
- Budget flights, hotels, and race fees in one place
The “app” part (accounts and trip plans) runs on Supabase. Ow, yeah! 🏄 But for the MVP content, I didn’t want everything living in a database.
Users would manually add races they know. That was the original idea: a lightweight trip planner where you plug in your next OCR and we help with flights, hotels, and budget.
Then I thought: what if someone doesn’t have a race yet and needs help finding one?
So I expanded the site with Obstacle Course Race Calendar — a public directory where anyone can:
- Browse obstacle course races by country and by city.
- See nearby airports and rough travel ideas
- Check typical weather for race dates
- Filter by brand and distance etc.
My first thought:
“Easy. Add a
racestable in Supabase and build an admin UI.”
My second thought:
Do I really want to put stuff in supabase?
So instead of a races table, I did this:
content/races/
├── spartan/
│ └── 2026/
│ ├── paris.mdx
│ └── london.mdx
├── tough-mudder/
│ └── 2025/
│ └── atlanta.mdx
└── ... 200+ files
Each file has frontmatter + MDX: brand, date, location, distances, coordinates, terrain, plus a description.
Velite reads those files at build time, validates them, and gives me a typed dataset I can import in Next.js.
What I avoided by not using Supabase for races
By not putting race data in the DB (yet), I skipped:
- Writing migrations every time I tweak race fields
- Building a custom admin panel just to edit content
- Managing seed scripts for dev/staging/prod
- Worrying about schema drift between environments
- Dealing with runtime bugs like “DB not reachable” for pages that could be static
For an MVP, that’s all extra surface area to maintain.
Instead, my workflow is:
- Edit a
.mdxfile - Commit
- Vercel rebuilds
-
/racesis up to date
Deployment = publishing.
How I solved it instead: MDX + Velite
Velite makes the flat-file approach feel surprisingly “database-like” — but without the DB.
At build time it:
- Validates content against a Zod/TS schema
- Generates TypeScript types
- Outputs an array I can import and filter
In code, it looks roughly like this (simplified):
import { races } from "@/content/generated";
const frenchSpartan = races.filter(
(race) =>
race.brand === "spartan" &&
race.country === "France"
);
I still get:
- Filters
- Searching
- Typed fields
- Autocomplete in my editor
“But what if I outgrow this?”
That’s the nice part: starting in files isn’t a trap.
If /races ever needs:
- Non-technical editors
- Live updates without deploys
- Super complex querying
…I can:
- Create a
racestable in Supabase - Write a one-off script that reads the MDX files and inserts them into the DB
- Switch from
import { races }toawait db.query.races
The early choice (files) doesn’t block the later choice (database).
It just lets me ship faster now.
Conclusion: default to files for MVP content
If you’re a founder or indie hacker sketching out an MVP, it’s worth pausing before you:
- model everything in SQL
- build an admin UI
- wire up forms to manage “content”
Ask:
- Will users actually write to this?
- Does it change constantly?
- Do I need querying tools for it right now?
If the answer is “not really”, try this stack:
Files in Git + a schema layer (Velite or similar) + static generation.
You can always move it into a database later.
But you might not have to.
Top comments (0)