MDDB GraphQL API Documentation

Overview

MDDB provides a fully-functional GraphQL API alongside its REST and gRPC interfaces. The GraphQL endpoint offers flexible querying, type safety, and schema introspection — every operation declared in services/mddbd/graphql/schema.graphql is wired to the in-process MCP DirectClient (the same code path REST/gRPC use), so behaviour is identical across protocols.

Endpoint: POST /graphql Playground: GET /playground (development tool, served alongside the endpoint)

Status (MDDB 2.9.11+): GraphQL is now enabled by default. Prior to 2.9.11, the resolvers were scaffolding stubs that panicked with not implemented for every query and mutation except login. As of 2.9.11 every query and mutation in schema.graphql has a real implementation that delegates through the same adapter as REST and gRPC. To opt out, set MDDB_GRAPHQL_ENABLED=false.

Quick Start

Enable / disable GraphQL

# Default — GraphQL on
./mddbd

# Explicit on
MDDB_GRAPHQL_ENABLED=true ./mddbd

# Disable (saves a few MB of resident memory + a route)
MDDB_GRAPHQL_ENABLED=false ./mddbd

# Docker
docker run tradik/mddb                                      # GraphQL on
docker run -e MDDB_GRAPHQL_ENABLED=false tradik/mddb        # GraphQL off

Access GraphQL Playground

Visit http://localhost:11023/playground to explore the API interactively. Introspection ({__schema{queryType{fields{name}}}}) is enabled.

Smoke test

# List supported queries
curl -X POST http://localhost:11023/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{__schema{queryType{fields{name}}}}"}'

# Real query (search documents in collection 'blog')
curl -X POST http://localhost:11023/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{search(input:{collection:\"blog\",limit:5}){totalCount edges{node{key lang}}}}"}'

Authentication

GraphQL uses the same JWT/API key authentication as the REST API.

Login Mutation

mutation {
  login(username: "admin", password: "your-password") {
    token
    expiresAt
  }
}

Using the Token

Include the token in subsequent requests:

HTTP Header:

Authorization: Bearer YOUR_JWT_TOKEN

Example with curl:

curl -X POST http://localhost:11023/graphql \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ me { username admin } }"}'

API Key Authentication

Alternatively, use an API key:

X-API-Key: mddb_live_YOUR_API_KEY

Schema Overview

Core Types

Document

type Document {
  id: ID!
  key: String!
  lang: String!
  meta: JSONObject!
  contentMd: String!
  addedAt: Time!
  updatedAt: Time!
  expiresAt: Time
}

VectorSearchResult

type VectorSearchResult {
  document: Document!
  score: Float!
  rank: Int!
}

User

type User {
  username: String!
  admin: Boolean!
  createdAt: Time!
}

Authentication Directives

  • @auth - Requires authentication (JWT or API key)
  • @hasRole(role: Role!) - Requires specific role (ADMIN or USER)

Queries

Get Current User

query {
  me {
    username
    admin
    createdAt
  }
}

Get Single Document

query {
  document(
    collection: "blog"
    key: "hello-world"
    lang: "en"
  ) {
    id
    key
    lang
    contentMd
    meta
    addedAt
    updatedAt
  }
}

With Template Variables:

query {
  document(
    collection: "templates"
    key: "email"
    lang: "en"
    env: { name: "John", year: "2024" }
  ) {
    contentMd
  }
}

Search Documents

query {
  search(input: {
    collection: "blog"
    filterMeta: [
      { key: "status", values: ["published"] }
      { key: "author", values: ["John", "Jane"] }
    ]
    sort: "updatedAt"
    asc: false
    limit: 10
    offset: 0
  }) {
    edges {
      cursor
      node {
        key
        lang
        contentMd
        meta
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      totalCount
    }
  }
}
query {
  vectorSearch(input: {
    collection: "kb"
    query: "how to configure authentication?"
    topK: 5
    threshold: 0.7
    includeContent: true
  }) {
    results {
      document {
        key
        lang
        contentMd
      }
      score
      rank
    }
    total
    model
    dimensions
  }
}

With Pre-computed Vector:

query {
  vectorSearch(input: {
    collection: "kb"
    queryVector: [0.1, 0.2, 0.3, ...]  # 1536-dim for OpenAI
    topK: 10
  }) {
    results {
      document { key contentMd }
      score
    }
  }
}
query {
  fts(input: {
    collection: "docs"
    query: "authentication jwt token"
    limit: 20
  }) {
    results {
      document {
        key
        contentMd
      }
      score
      matchedTerms
    }
    total
  }
}

Database Statistics

query {
  stats {
    databasePath
    databaseSize
    mode
    totalDocuments
    totalRevisions
    collections {
      name
      documentCount
      revisionCount
    }
  }
}

Vector Statistics

query {
  vectorStats {
    enabled
    provider
    model
    dimensions
    indexReady
    collections
  }
}

List Webhooks (Admin Only)

Optionally filter by collection:

query {
  webhooks(collection: "blog") {
    id
    url
    events
    collection
    createdAt
  }
}

List Schemas

query {
  schemas {
    collection
    schema
    enabled
  }
}

List Users (Admin Only)

query {
  users {
    username
    admin
    createdAt
  }
}

List Groups (Admin Only)

query {
  groups {
    name
    description
    members
    createdAt
  }
}

User Permissions (Admin Only)

query {
  userPermissions(username: "john") {
    username
    collection
    read
    write
    admin
  }
}

Group Permissions (Admin Only)

query {
  groupPermissions(groupName: "editors") {
    groupName
    collection
    read
    write
    admin
  }
}

Mutations

Login

mutation {
  login(username: "admin", password: "secret") {
    token
    expiresAt
  }
}

Add Document

mutation {
  addDocument(input: {
    collection: "blog"
    key: "my-post"
    lang: "en"
    meta: [
      { key: "author", values: ["John"] }
      { key: "tags", values: ["tutorial", "graphql"] }
      { key: "status", values: ["published"] }
    ]
    contentMd: """
    # My Blog Post

    This is the content of my post.
    """
    ttl: 3600  # optional: expires in 1 hour
  }) {
    id
    key
    addedAt
  }
}

Update Document

mutation {
  updateDocument(input: {
    collection: "blog"
    key: "my-post"
    lang: "en"
    meta: [
      { key: "status", values: ["draft"] }
    ]
    contentMd: "# Updated Content"
  }) {
    id
    updatedAt
  }
}

Delete Document

mutation {
  deleteDocument(
    collection: "blog"
    key: "my-post"
    lang: "en"
  )
}

Delete Collection

mutation {
  deleteCollection(collection: "old-blog")
}

Reindex Vectors

mutation {
  vectorReindex(
    collection: "kb"
    force: false
  ) {
    enabled
    collections
  }
}

Add Batch

Add multiple documents to a collection in one request:

mutation {
  addBatch(
    collection: "blog"
    documents: [
      {
        key: "post-1"
        lang: "en"
        contentMd: "# First Post"
        meta: [{ key: "status", values: ["published"] }]
        saveRevision: true
      }
      {
        key: "post-2"
        lang: "en"
        contentMd: "# Second Post"
      }
    ]
  ) {
    added
    updated
    failed
    errors
  }
}

Ingest Documents

Bulk ingest with per-batch options (duplicate skipping, embedding/FTS/webhook control):

mutation {
  ingestDocuments(
    collection: "scraped"
    documents: [
      {
        url: "https://example.com/article"
        key: "article-1"
        lang: "en"
        contentMd: "# Scraped Article"
        meta: [{ key: "source", values: ["crawler"] }]
        extractFrontmatter: true
        scraper: "my-crawler"
        ttl: 86400
      }
    ]
    options: {
      skipDuplicates: true
      skipEmbeddings: false
      skipWebhooks: true
      autoConfigureCollection: true
      saveRevision: false
    }
  ) {
    added
    updated
    skipped
    failed
    errors
    collection
    durationMs
  }
}

Set TTL

mutation {
  setTTL(
    collection: "cache"
    key: "session-data"
    lang: "en"
    ttl: 3600
  ) {
    id
    expiresAt
  }
}

Import from URL

mutation {
  importURL(
    collection: "articles"
    url: "https://example.com/page.md"
    key: "imported-page"
    lang: "en"
    meta: [{ key: "source", values: ["import"] }]
    ttl: 86400
  ) {
    id
    key
    addedAt
  }
}

Register Webhook (Admin Only)

mutation {
  registerWebhook(input: {
    url: "https://example.com/hook"
    events: ["insert", "update", "delete"]
    collection: "blog"
  }) {
    id
    url
    events
    collection
    createdAt
  }
}

Delete Webhook (Admin Only)

mutation {
  deleteWebhook(id: "webhook-id")
}

Set Schema (Admin Only)

The schema argument is a JSON Schema document as a string:

mutation {
  setSchema(input: {
    collection: "blog"
    schema: "{\"required\":[\"author\"],\"properties\":{\"author\":{\"type\":\"string\"}}}"
  })
}

Delete Schema (Admin Only)

mutation {
  deleteSchema(collection: "blog")
}

Validate Document

Validate metadata against a collection's schema without writing:

mutation {
  validateDocument(
    collection: "blog"
    meta: [
      { key: "author", values: ["John"] }
      { key: "status", values: ["published"] }
    ]
  ) {
    valid
    errors
  }
}

Admin Operations

Register User (Admin Only)

mutation {
  register(
    username: "newuser"
    password: "secure-password"
  ) {
    username
    admin
    createdAt
  }
}

Create API Key

mutation {
  createAPIKey(input: {
    description: "My API key"
    expiresAt: 1735689600  # Unix timestamp, optional
  }) {
    key
    description
    expiresAt
    createdAt
  }
}

Create Group

mutation {
  createGroup(input: {
    name: "editors"
    description: "Content editors"
    members: ["user1", "user2"]
  }) {
    name
    description
    members
  }
}

Update Group

Replaces the group's description and member list:

mutation {
  updateGroup(
    name: "editors"
    description: "Content editors and reviewers"
    members: ["user1", "user2", "user3"]
  ) {
    name
    description
    members
  }
}

Delete Group

mutation {
  deleteGroup(name: "editors")
}

Set Permission

mutation {
  setPermission(input: {
    username: "john"
    collection: "blog"
    read: true
    write: true
    admin: false
  })
}

Set Group Permission

mutation {
  setGroupPermission(input: {
    groupName: "editors"
    collection: "blog"
    read: true
    write: true
    admin: false
  })
}

Advanced Usage

Combining Multiple Queries

GraphQL allows fetching multiple resources in one request:

query {
  currentUser: me {
    username
    admin
  }

  recentPosts: search(input: {
    collection: "blog"
    sort: "updatedAt"
    limit: 5
  }) {
    edges {
      node {
        key
        contentMd
      }
    }
  }

  dbStats: stats {
    totalDocuments
    databaseSize
  }
}

Field Selection

Request only the fields you need:

query {
  document(collection: "blog", key: "post", lang: "en") {
    key        # Only these 2 fields returned
    contentMd
  }
}

Variables

Use GraphQL variables for dynamic queries:

query GetDocument($coll: String!, $key: String!, $lang: String!) {
  document(collection: $coll, key: $key, lang: $lang) {
    id
    contentMd
  }
}

Variables:

{
  "coll": "blog",
  "key": "my-post",
  "lang": "en"
}

Error Handling

GraphQL returns structured errors:

{
  "errors": [
    {
      "message": "authentication failed: invalid credentials",
      "path": ["login"]
    }
  ],
  "data": null
}

Common error messages:

  • "unauthenticated: missing or invalid credentials" — no JWT/API key in the request and auth is enabled
  • "forbidden: admin privileges required" — admin-only operation called without admin claims
  • "permission denied" — caller authenticated but lacks read/write/admin on the target collection
  • "authentication failed" / "invalid credentials"login mutation rejected the username/password

Performance Considerations

Query Complexity

MDDB limits query complexity to prevent resource exhaustion:

  • Max complexity: 1000 points
  • Max depth: 10 levels

Pagination

Use cursor-based pagination for large result sets:

query {
  search(input: { collection: "blog", limit: 50 }) {
    edges {
      cursor
      node { key }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Caching

Parsed GraphQL queries are cached in memory (LRU cache).

GraphQL vs REST vs gRPC

Feature GraphQL REST gRPC
Field selection ✅ Flexible ❌ Fixed ❌ Fixed
Multiple resources ✅ Single request ❌ Multiple requests ✅ Streaming
Type safety ✅ Schema ❌ None ✅ Protobuf
Introspection ✅ Built-in ❌ None ✅ Reflection
Binary protocol ❌ JSON ❌ JSON ✅ Protobuf
Streaming ⚠️ Limited ❌ No ✅ Yes
Simplicity ⚠️ Learning curve ✅ Simple ⚠️ Setup

Use GraphQL when:

  • You need flexible field selection
  • You want to combine multiple queries
  • You're building a modern frontend (React, Vue, Angular)
  • You prefer strong typing and schema validation

Use REST when:

  • You need simple curl/wget access
  • You're building quick scripts or automations
  • You need streaming responses (export, backup)

Use gRPC when:

  • You need maximum performance
  • You're building service-to-service communication
  • You need bidirectional streaming

Configuration

Environment Variables

  • MDDB_GRAPHQL_ENABLED - Enable/disable GraphQL (default: false)
  • MDDB_GRAPHQL_PLAYGROUND - Enable/disable Playground (default: true)
  • MDDB_AUTH_ENABLED - Enable authentication (default: false)
  • MDDB_AUTH_JWT_SECRET - JWT secret (required if auth enabled)

CLI Flags

./mddbd --graphql                    # Enable GraphQL
./mddbd --graphql --playground=false # Disable Playground

Client Libraries

JavaScript/TypeScript

npm install @apollo/client graphql
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:11023/graphql',
  cache: new InMemoryCache(),
  headers: {
    authorization: `Bearer ${token}`
  }
});

const { data } = await client.query({
  query: gql`
    query {
      stats {
        totalDocuments
      }
    }
  `
});

Python

pip install gql
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport

transport = RequestsHTTPTransport(
    url='http://localhost:11023/graphql',
    headers={'Authorization': f'Bearer {token}'}
)

client = Client(transport=transport, fetch_schema_from_transport=True)

query = gql('''
    query {
        stats {
            totalDocuments
        }
    }
''')

result = client.execute(query)

Troubleshooting

GraphQL endpoint not responding

Check if GraphQL is enabled:

curl http://localhost:11023/v1/config | jq .graphql_enabled

Authentication errors

Verify your token:

curl -X POST http://localhost:11023/graphql \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ me { username } }"}'

Schema introspection

Query the schema directly:

query {
  __schema {
    types {
      name
    }
  }
}

Future Enhancements

The following features are planned for future releases:

  • Subscriptions for real-time updates
  • Advanced filtering with logical operators
  • Rate limiting per user/IP
  • DataLoader for N+1 query prevention

Contributing

GraphQL resolver implementations are in progress. Contributions welcome:

  • services/mddbd/graphql/schema.resolvers.go - Resolver implementations
  • services/mddbd/graphql_adapter.go - Server adapter

Support

  • GitHub Issues: https://github.com/tradik/mddb/issues
  • Documentation: https://github.com/tradik/mddb/tree/main/docs
  • REST API Docs: /v1/endpoints

Version: 2.7.1 Last Updated: March 2026