Flatmark is a Node library that indexes a folder of markdown files and gives you a chainable query API over the frontmatter. Here's what it does, where it beats Velite and Astro content collections, and the four things I'd want fixed before shipping it.
Every project that keeps content as markdown files hits the same wall. You have a folder of .md files with YAML at the top, and the second you want something as boring as "the ten newest posts that aren't drafts", you're writing your own loop over fs.readdir, parsing frontmatter, filtering, sorting, and then caching the result so you don't redo it on every request.
I found flatmark this week. It does that part for you. It reads a folder of markdown files, indexes the frontmatter in memory, and hands you a chainable query API over it.
const db = new FlatMark('./content')
await db.load()
const posts = db.collection('posts')
.where({ draft: false })
.orderBy('date', 'desc')
.limit(10)
.get()
That's the whole pitch. No server, no migrations, no build step to wire into your framework.
Why this gap exists in the first place
The library everyone used for this in Next.js was Contentlayer. Its README now opens with a line saying it is no longer maintained due to lack of funding. The last release, 0.3.4, shipped in June 2023. It still carries 3.5k stars and 90 open issues, so people keep finding it, installing it, and then filing build errors against starter templates that still depend on it.
What replaced it mostly went one direction: build time. Velite compiles your markdown into JSON and typed exports before your app runs. Astro's content collections do the same job inside Astro, and got faster again when Astro 7 moved the markdown pipeline to Rust. Both are good tools. Both are also a compile step that produces read-only output, tied to a bundler or a framework.
Flatmark sits somewhere else. It runs at runtime, in plain Node, and it writes back to disk.
What you actually get
One folder is one collection. Drop content/posts/hello.md on disk and it becomes a record in db.collection('posts'). Every frontmatter key lands at the top level of the record, alongside the fields flatmark adds itself: _id (the filename without the extension), _path, and _body (the markdown after the frontmatter).
The query side is small and predictable. .where(), .select(), .orderBy(), .limit(), .offset() to build a query, then .get(), .first() or .count() to run it. Filters take operators that will look familiar if you have ever touched MongoDB: $gt, $gte, $lt, $lte, $ne, $includes for arrays, and $exists for checking whether a field is there at all.
db.collection('posts')
.where({ tags: { $includes: 'typescript' } })
.where({ views: { $gte: 100 } })
.where({ hero: { $exists: true } })
.orderBy('date', 'desc')
.get()
Multiple .where() calls combine with AND. There is no OR, and no joins.
Writes are the part that separates it from the build-time crowd:
await db.collection('posts').insert({
_id: 'my-new-post',
title: 'My New Post',
date: '2026-06-01',
draft: true,
_body: '# My New Post\n\nContent here.'
})
await db.collection('posts').update('my-new-post', { draft: false })
await db.collection('posts').delete('my-new-post')
insert creates the file, update patches only the frontmatter fields you pass, delete removes it. Those three hit the disk and are async. Everything on the read side is synchronous once load() has finished, because the index is already sitting in memory.
Two extras worth knowing. You can pass a Zod schema per collection and flatmark will throw a FlatMarkValidationError when frontmatter doesn't match, which is the thing you actually want when a teammate ships a post with no date. And load({ watch: true }) keeps the index in sync while files change on disk, which is what you want in dev and almost never what you want in production.
Where I'd use this
The obvious one is a blog or docs site where the content lives in the repo and you got tired of your own getAllPosts() helper growing a sort argument, then a tag filter, then a pagination offset. Flatmark is that helper, written once, by someone else, with types.
The less obvious one, and the reason it caught my eye: a vault. I run my whole working life out of an Obsidian vault of markdown notes, and inside Obsidian I query it with Bases, which I wrote about in Dataview vs Datacore vs Bases. What I don't have is a clean way to run those same queries from a script outside Obsidian. Flatmark is exactly that shape: point it at the vault folder, ask for every note where status is open, get an array back. Same idea as the Cowork and Obsidian second brain setup, except any Node process can read and write it.
Third case: an internal tool where a few hundred records really don't need Postgres. Team pages, a changelog, a small directory. Files in git, reviewed in pull requests, no database to back up.
The four things I'd want before I put this in client work
I'm not going to pretend this is production-ready, because it isn't yet, and the version number says so.
It's at 0.1.0. One publish, June 2026, 10 stars, 0 forks, 23 commits, one author. Three real dependencies (gray-matter, chokidar, and Zod as an optional peer). That's a clean, small package, and it's also a package that could stop being maintained next month. Contentlayer had 3.5k stars and still died.
Everything is a linear scan. The index is in memory and .where() walks it. For a few hundred files that's free. Nobody has published a number for what happens at ten or fifty thousand, and the README doesn't claim one. Test it against your own content before you assume.
The writes rule out most serverless. Vercel functions, Lambda and friends give you a read-only filesystem apart from /tmp. Flatmark's insert, update and delete write real files next to your source, so they only make sense on a long-lived server, a container, or a local script. Reads are fine anywhere, because they happen after load().
No full-text search, no OR, no joins. Frontmatter filtering only. _body is there as a string and you can search it yourself, but there is no index behind it. If your requirement is "search the article text", this isn't the tool.
None of that makes it a bad library. It makes it a young one, aimed squarely at a job that a lot of people currently do with fifty lines of copy-pasted glue code.
Frequently asked questions
What is flatmark?
Flatmark is an open-source Node library that turns a folder of markdown files into a queryable database. It parses YAML frontmatter from each .md file, builds an in-memory index when you call load(), and gives you a chainable API to filter, sort and paginate the results. It also writes back, so you can create, update and delete markdown files through the same API. It's MIT licensed and installs as @flatmark/core.
Is flatmark a replacement for Contentlayer?
It covers a lot of the same ground, but they work differently. Contentlayer was a build-time step that generated typed data for your Next.js app, and it has been unmaintained since 2023. Flatmark runs at runtime in any Node project, with no compile step and no framework tie-in, and it can write files as well as read them. If you want a like-for-like Contentlayer replacement with typed build output, look at Velite first.
Do I need a database to query markdown files?
No. For a few hundred to a few thousand markdown files, reading them into memory once and querying the array is fast enough and far less work than running a database. You only need a real database when you need full-text search, relational joins, concurrent writes from multiple processes, or a dataset too large to hold in memory. Flatmark covers the first case, not the others.
Can I use flatmark with Next.js or Astro?
Yes for reading. Flatmark is plain Node with no framework dependency, so you can call it from a Next.js server component, an Astro build script, or any API route. Writing is the constraint: insert, update and delete need a writable filesystem, which rules out most serverless hosting. On Vercel or Lambda, read with flatmark and handle writes some other way.
How does flatmark compare to Astro content collections?
Astro content collections are built into Astro, run at build time, give you typed schemas via Zod, and produce read-only content. Flatmark is framework-agnostic, runs at runtime, uses Zod schemas optionally, and supports writes. If you're already on Astro and only reading content, the built-in collections are the better choice. Flatmark makes sense when you need the same queries outside Astro, or when something needs to write markdown back.
Is flatmark ready for production?
Not yet, in my view. It's at version 0.1.0 with a single publish, 10 GitHub stars and one maintainer as of September 2026. The code is small and the dependency list is short, which helps, but there are no published benchmarks and no track record. Try it on a personal project or an internal tool first, and pin the version.
What to do now
If you have a lib/posts.ts file in a side project that has quietly grown to 120 lines of frontmatter parsing, spend twenty minutes replacing it with flatmark and see whether the queries you actually run survive the operator list. That's the honest test, and it costs almost nothing.
If you're on Astro or already compiling content with Velite and only ever reading it, stay where you are. This doesn't buy you anything.
And if, like me, you're sitting on a few thousand markdown notes you can only query from inside your note-taking app, this is the most interesting thing on the list. That's the script I'm writing next.