mdxdb
Local database for MDX files with sync to .do platform
MDXDB: MDX as Database
MDXDB exposes your .mdx files as a local database - query, filter, and search your content like a database, with optional sync to the .do platform backend.
What is MDXDB?
Think of MDXDB as turning your local ./content directory into a queryable database:
# Your local MDX files
./content/
blog/post-1.mdx # Row in 'blog' collection
blog/post-2.mdx # Row in 'blog' collection
products/widget.mdx # Row in 'products' collection
# Query like a database
mdxdb query "SELECT * FROM blog WHERE published = true"MDXDB provides:
- Local Database: Your
.mdxfiles ARE the database - Query Collections: Filter, sort, paginate like SQL/MongoDB
- Full-Text Search: Search across all content
- Live Queries: Subscribe to file changes in real-time
- Indexes: Build indexes for fast queries
- Remote Sync: Push/pull to .do platform backend
- Type Safety: Full TypeScript support with Zod schemas
Local-First Architecture
MDXDB is local-first: your .mdx files are the source of truth, stored on your filesystem:
Local (source of truth) Remote (optional sync)
├── content/ ├── platform.do backend
│ ├── blog/*.mdx ←────→ │ ├── blog collection
│ ├── products/*.mdx ←────→ │ ├── products collection
│ └── docs/*.mdx ←────→ │ └── docs collectionBenefits:
- Work offline: Query your data without internet
- Git-friendly: Content is version-controlled files
- Fast: No network latency for queries
- Portable: Move your database by moving files
- Transparent: Every record is a readable
.mdxfile
Installation
pnpm install mdxdbAuthentication
To sync with the .do platform backend, you need to authenticate:
DO_TOKEN (API Token)
Set your platform API token:
# Export token
export DO_TOKEN="your-api-token-here"
# Or in .env file
echo "DO_TOKEN=your-api-token-here" >> .env
# Now you can push/pull to platform
mdxdb push
mdxdb pullGet your token from platform.do/settings/tokens.
OAuth via OAuth.do
For interactive sessions:
# Login via OAuth
mdxdb login
# Opens browser to oauth.do for authentication
# Credentials stored securely
# Now authenticated
mdxdb push
mdxdb pull
mdxdb sync --watchAuthentication in Code
import { mdxdb } from 'mdxdb'
// With DO_TOKEN
const db = await mdxdb.init({
source: './content',
sync: {
enabled: true,
url: 'https://api.platform.do',
token: process.env.DO_TOKEN,
},
})
// With OAuth
const db = await mdxdb.init({
source: './content',
sync: {
enabled: true,
url: 'https://api.platform.do',
auth: 'oauth',
},
})Required Scopes
OAuth scopes needed for sync:
collections:read- Query remote collectionscollections:write- Push/update collectionscollections:delete- Delete remote data
# Login with specific scopes
mdxdb login --scope "collections:read collections:write"Quick Start
Initialize Database (Local)
import { mdxdb } from 'mdxdb'
// Initialize from local content directory
const db = await mdxdb.init({
source: './content',
collections: ['posts', 'products', 'docs'],
})Initialize Database (With Remote Sync)
import { mdxdb } from 'mdxdb'
// Initialize with platform sync
const db = await mdxdb.init({
source: './content',
collections: ['posts', 'products', 'docs'],
sync: {
enabled: true,
url: 'https://api.platform.do',
token: process.env.DO_TOKEN,
autoPush: true,
autoPull: true,
},
})
// Changes automatically sync to platform.doQuery Collections
// List all posts
const posts = await db.posts.list()
// Filter and sort
const publishedPosts = await db.posts.list({
where: { published: true, category: 'tutorial' },
sort: '-date', // descending by date
limit: 10,
})
// Pagination
const page = await db.posts.list({
skip: 20,
limit: 10,
})Get Single Document
// Get by slug
const post = await db.posts.findOne({
where: { slug: 'getting-started' },
})
// Get by ID
const product = await db.products.get('https://products.example.com/widget-x')CLI Commands
Initialize Database
Create database from content directory:
mdxdb init ./content --collections posts,products,docsQuery from Command Line
# List all posts
mdxdb query posts
# Filter posts
mdxdb query posts --where 'published=true' --sort '-date' --limit 10
# Get single post
mdxdb query posts --where 'slug=getting-started'Build Indexes
# Build all indexes
mdxdb index ./content
# Build specific collection
mdxdb index ./content/posts --collection postsWatch for Changes
# Watch and rebuild indexes on changes
mdxdb watch ./contentCollection Configuration
Define collections with schemas:
import { defineCollection } from 'mdxdb'
import { z } from 'zod'
export const posts = defineCollection({
name: 'posts',
source: './content/blog',
schema: z.object({
$type: z.literal('BlogPost'),
title: z.string(),
author: z.string(),
date: z.string().datetime(),
tags: z.array(z.string()),
published: z.boolean(),
}),
indexes: ['date', 'author', 'tags'],
})Query Operations
Filtering
// Exact match
const posts = await db.posts.list({
where: { category: 'tutorial' },
})
// Multiple conditions (AND)
const posts = await db.posts.list({
where: {
category: 'tutorial',
published: true,
author: 'Jane Developer',
},
})
// Comparison operators
const posts = await db.posts.list({
where: {
date: { $gte: '2025-01-01', $lte: '2025-12-31' },
views: { $gt: 1000 },
},
})
// Array operators
const posts = await db.posts.list({
where: {
tags: { $contains: 'typescript' },
categories: { $in: ['tutorial', 'guide'] },
},
})Sorting
// Ascending
const posts = await db.posts.list({
sort: 'date', // or { date: 1 }
})
// Descending
const posts = await db.posts.list({
sort: '-date', // or { date: -1 }
})
// Multiple fields
const posts = await db.posts.list({
sort: { date: -1, title: 1 },
})Pagination
// Limit
const posts = await db.posts.list({
limit: 10,
})
// Skip + Limit
const posts = await db.posts.list({
skip: 20,
limit: 10,
})
// Cursor-based pagination
const page1 = await db.posts.list({ limit: 10 })
const page2 = await db.posts.list({
cursor: page1.cursor,
limit: 10,
})Full-Text Search
Basic Search
// Search across all fields
const results = await db.search('typescript tutorial')
// Search specific collection
const posts = await db.posts.search('graphql api')Advanced Search
// Search with filters
const results = await db.posts.search('typescript', {
where: { published: true },
sort: '-date',
limit: 20,
})
// Search specific fields
const results = await db.posts.search('typescript', {
fields: ['title', 'description'],
})
// Fuzzy search
const results = await db.posts.search('typscript', {
fuzzy: true,
maxDistance: 2,
})Live Queries
Watch Collection
// Watch all changes
const unsubscribe = db.posts.watch((posts) => {
console.log('Posts updated:', posts.length)
renderPosts(posts)
})
// Cleanup
unsubscribe()Subscribe to Query
// Watch filtered query
const unsubscribe = db.posts.subscribe(
{ where: { category: 'tech', published: true } },
(posts) => {
console.log('Tech posts updated:', posts.length)
renderPosts(posts)
}
)Single Document
// Watch single document
const unsubscribe = db.posts.watchOne(
{ where: { slug: 'getting-started' } },
(post) => {
console.log('Post updated:', post.title)
renderPost(post)
}
)Indexes
Define Indexes
export const posts = defineCollection({
name: 'posts',
indexes: [
'date', // Single field
'author',
['category', 'date'], // Compound index
{ field: 'tags', type: 'array' }, // Array index
{ field: 'content', type: 'text' }, // Full-text index
],
})Build Indexes
// Build all indexes
await db.buildIndexes()
// Build specific collection
await db.posts.buildIndex()
// Rebuild indexes
await db.posts.rebuildIndex()Relationships
References
// Document with reference
const post = await db.posts.findOne({
where: { slug: 'getting-started' },
})
// Get referenced author
const author = await db.authors.get(post.author.$id)
// Get all related
const relatedPosts = await db.posts.list({
where: { author: { $id: author.$id } },
})Populate References
// Populate single reference
const post = await db.posts.findOne(
{ where: { slug: 'getting-started' } },
{ populate: ['author'] }
)
console.log(post.author.name) // Populated
// Populate multiple references
const post = await db.posts.findOne(
{ where: { slug: 'getting-started' } },
{ populate: ['author', 'category', 'relatedPosts'] }
)Aggregations
Count
// Count all
const count = await db.posts.count()
// Count with filter
const publishedCount = await db.posts.count({
where: { published: true },
})Group By
// Group by category
const groups = await db.posts.groupBy('category')
// [{ category: 'tutorial', count: 42 }, ...]
// Group with aggregations
const stats = await db.posts.aggregate({
groupBy: 'author',
compute: {
postCount: { $count: true },
avgViews: { $avg: 'views' },
totalViews: { $sum: 'views' },
},
})Remote Datasources
MDXDB can query remote datasources alongside your local files:
import { defineConfig } from 'mdxdb'
export default defineConfig({
collections: [posts, products, docs],
// Add remote datasources
remotes: [
{
name: 'platform',
url: 'https://api.platform.do',
collections: ['users', 'organizations'],
},
{
name: 'warehouse',
url: 'https://warehouse.example.com/api',
collections: ['inventory', 'orders'],
},
],
})Query local and remote together:
// Query local blog posts
const localPosts = await db.blog.list()
// Query remote users
const users = await db.remotes.platform.users.list()
// Join local and remote
const postsWithAuthors = await db.blog.list({
populate: {
author: db.remotes.platform.users,
},
})Sync with .do Platform
Just like mdxe deploys your MDX files as sites/pages/apps on the .do platform, mdxdb syncs your MDX files as data collections to the platform backend.
Push Local to Platform
Deploy your local MDX database to .do platform:
# Push all collections
mdxdb push
# Push specific collections
mdxdb push blog products
# Push with transform
mdxdb push blog --transform "add SEO metadata"
# Dry run (preview changes)
mdxdb push --dry-runThis is similar to mdxe deploy but for data instead of apps:
# mdxe: Deploy MDX files as apps/sites
mdxe deploy ./content --name my-site
# mdxdb: Deploy MDX files as data collections
mdxdb push ./content --name my-dataPull Platform to Local
Sync data from platform to local files:
# Pull all collections
mdxdb pull
# Pull specific collections
mdxdb pull blog products
# Pull and create local MDX files
mdxdb pull blog --create-files
# Pull with filters
mdxdb pull blog --where "published=true"Two-Way Sync
Keep local and remote in sync:
# Watch and sync changes
mdxdb sync --watch
# Sync specific collections
mdxdb sync blog products --watch
# Conflict resolution
mdxdb sync --strategy local-wins # or remote-wins, manualSync Configuration
Configure automatic sync:
export default defineConfig({
collections: [posts, products],
// Sync to .do platform
sync: {
enabled: true,
url: 'https://api.platform.do',
apiKey: process.env.PLATFORM_API_KEY,
// Auto-push on file changes
autoPush: true,
// Auto-pull on remote changes
autoPull: true,
// Conflict resolution
conflictStrategy: 'local-wins',
// Which collections to sync
collections: ['blog', 'products'],
},
})With auto-sync enabled:
# Edit local file
vim blog/my-post.mdx
# Automatically pushed to platform.do ✓
# Remote change on platform.do
# Automatically pulled to local ✓Configuration
Create mdxdb.config.ts:
import { defineConfig } from 'mdxdb'
import { posts, products, docs } from './collections'
export default defineConfig({
// Collections
collections: [posts, products, docs],
// Source directory
source: './content',
// Index directory
indexDir: './.mdxdb',
// Watch for changes
watch: true,
// Build indexes on init
buildIndexes: true,
// Cache settings
cache: {
enabled: true,
ttl: 3600, // 1 hour
},
})Integrations
Fumadocs
Use with Fumadocs:
import { mdxdb } from 'mdxdb'
import { createSource } from 'fumadocs-mdx'
const db = await mdxdb.init({ source: './content' })
export const source = createSource({
loader: async () => {
const docs = await db.docs.list({ sort: 'order' })
return docs.map(doc => ({
slug: doc.slug,
data: doc,
}))
},
})Payload CMS
Sync with Payload CMS:
import { mdxdb } from 'mdxdb'
import payload from 'payload'
const db = await mdxdb.init({ source: './content' })
// Watch for changes and sync to Payload
db.posts.watch(async (posts) => {
for (const post of posts) {
await payload.update({
collection: 'posts',
where: { slug: { equals: post.slug } },
data: post,
})
}
})MDXDB vs MDXE
Both tools deploy your MDX files to the .do platform, but for different purposes:
| Aspect | MDXDB | MDXE |
|---|---|---|
| Purpose | Data collections | Sites/apps/pages |
| Deploy | MDX → Database records | MDX → HTML/routes |
| Local | Query files as database | Develop with hot reload |
| Remote | Sync to platform backend | Deploy to Workers/edge |
| Access | Query API, SDK | HTTP routes, URLs |
| Use case | Content management | Web applications |
Example Workflow
# 1. Create MDX content locally
mkdir content
echo "---\ntitle: My Post\n---\n# Content" > content/blog/post.mdx
# 2. Query locally with mdxdb
mdxdb query "SELECT * FROM blog"
# 3. Deploy content as data to platform
mdxdb push blog
# 4. Deploy content as site to platform
mdxe deploy ./content --name my-blog
# Result:
# - mdxdb: platform.do API can query blog posts as data
# - mdxe: my-blog.platform.do serves posts as web pagesCombined Example
Use both together for a full-stack app:
// Frontend (deployed with mdxe)
import { db } from 'mdxdb'
export default function BlogPage() {
// Query data from platform.do backend
const posts = await db.remotes.platform.blog.list({
where: { published: true },
sort: '-date',
})
return (
<div>
{posts.map(post => (
<article key={post.$id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}Use Cases
Documentation Sites
Query docs with instant search:
const db = await mdxdb.init({
source: './docs',
collections: ['guides', 'api-reference', 'examples'],
})
// Full-text search
const results = await db.search(query)
// Navigate by category
const guides = await db.guides.list({
where: { category: 'getting-started' },
sort: 'order',
})Blog
Dynamic blog with filters:
// Get posts by tag
const posts = await db.posts.list({
where: { tags: { $contains: 'typescript' } },
sort: '-date',
limit: 10,
})
// Get author's posts
const authorPosts = await db.posts.list({
where: { 'author.email': '[email protected]' },
})Product Catalog
Queryable product database:
// Filter products
const products = await db.products.list({
where: {
category: 'electronics',
price: { $lte: 500 },
inStock: true,
},
sort: { price: 1 },
})
// Search products
const results = await db.products.search('wireless headphones')