.do
Mdxmdxdb

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 .mdx files 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 collection

Benefits:

  • 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 .mdx file

Installation

pnpm install mdxdb

Authentication

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 pull

Get 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 --watch

Authentication 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 collections
  • collections:write - Push/update collections
  • collections: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.do

Query 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,docs

Query 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 posts

Watch for Changes

# Watch and rebuild indexes on changes
mdxdb watch ./content

Collection 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,
})
// Search across all fields
const results = await db.search('typescript tutorial')

// Search specific collection
const posts = await db.posts.search('graphql api')
// 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-run

This 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-data

Pull 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, manual

Sync 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:

AspectMDXDBMDXE
PurposeData collectionsSites/apps/pages
DeployMDX → Database recordsMDX → HTML/routes
LocalQuery files as databaseDevelop with hot reload
RemoteSync to platform backendDeploy to Workers/edge
AccessQuery API, SDKHTTP routes, URLs
Use caseContent managementWeb 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 pages

Combined 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')

Next Steps