Guides

GraphQL API

Every project gets a GraphQL schema, generated automatically from its collections and declared relationships - no schema to write or maintain by hand.

Note
Read-only for now. The generated schema exposes Query fields only - writes still go through the REST data endpoints. A GraphQL mutation layer is a planned follow-up, not yet available.

Endpoint

A single POST endpoint per project runs any query against that project's current schema. The schema is rebuilt on every request from the project's live collections and relationships, so a field you just added in the console is queryable on your very next call:

bash
POST /api/projects/{projectId}/graphql
Content-Type: application/json
X-API-Key: YOUR_API_KEY

{
  "query": "{ posts { id title comments { body } } }"
}
POST /api/projects/{projectId}/graphql
Content-Type: application/json
X-API-Key: YOUR_API_KEY

{
  "query": "{ posts { id title comments { body } } }"
}

Authentication is the same as any other project data request - an API key (X-API-Key) or a Bearer session token. The route requires project read access, the same as GET data reads; there is no separate, weaker GraphQL auth path.

Generated schema shape

Each collection becomes a GraphQL object type (PascalCased and singularized - a posts collection becomes type Post), with every declared field mapped to a scalar (numberFloat, boolean Boolean, everything else, including date, → String, dates as ISO-8601), plus id, createdAt, and updatedAt. Two query fields are generated per collection:

graphql
# List - camelCased plural of the collection slug
posts(filter: String, sort: String, limit: Int, offset: Int): [Post]

# Single by id - camelCased singular (falls back to "<name>ById" if that
# would collide with the list field name)
post(id: ID!): Post
# List - camelCased plural of the collection slug
posts(filter: String, sort: String, limit: Int, offset: Int): [Post]

# Single by id - camelCased singular (falls back to "<name>ById" if that
# would collide with the list field name)
post(id: ID!): Post

filter takes the same JSON-encoded Mongo filter as REST's ?filter=. sort mirrors REST's ?sort= (e.g. -createdAt). limit defaults to 20 and is capped at 100; offset defaults to 0. A project with no collections declared yet still returns a valid (placeholder) schema rather than erroring on introspection.

Relationships as fields

Every declared relationship becomes a field on its source type, resolved through the exact same permission-scoped populate engine ?populate uses - not a separate implementation. A one-to-many or many-to-many relationship resolves to a list; many-to-one and one-to-one resolve to a single object or null. Nested relationship fields are batched per request (one query per level, regardless of how many parent rows), so this is cheap even for a deeply-linked query:

graphql
{
  posts {
    id
    title
    comments {
      body
      author {
        name
      }
    }
    tags {
      name
    }
  }
}
{
  posts {
    id
    title
    comments {
      body
      author {
        name
      }
    }
    tags {
      name
    }
  }
}

Permissions

GraphQL enforces the identical permission model REST does, field by field. A query field for a collection the caller cannot read fails with a FORBIDDEN error. A relationship field the caller cannot read - or that declares an allowedRoles restriction the caller doesn't satisfy - resolves to null or [] rather than a leaked document or an error, exactly like ?populate.

Query limits

A generated schema has no built-in cost limiter from the GraphQL spec itself, so the following bounds are enforced on every request before execution:

  • Max query depth: 8 selection levels.
  • Max field/alias count: 30 per operation.
  • Max parsed tokens: 10,000 per query document.
  • Execution timeout: 10 seconds.
  • Rate limit: 100 requests per 60 seconds per caller, per project.

A query that exceeds the depth or alias budget is rejected with 400 and a QUERY_TOO_COMPLEX error code before any schema or database work runs. A query that runs past the execution timeout returns a QUERY_TIMEOUT error in the standard GraphQL errors array.

Response shape and errors

Standard GraphQL-over-HTTP semantics. A malformed request (missing or non-string query) is a 400. Once the query is at least parseable, execution responds 200, with any syntax, validation, or resolver errors in the standard { data, errors } shape every GraphQL client already expects - a per-field resolver failure is a normal partial result, not a transport-level failure.

json
{
  "data": {
    "posts": [
      { "id": "665f0a...c31", "title": "Hello world", "comments": [] }
    ]
  }
}
{
  "data": {
    "posts": [
      { "id": "665f0a...c31", "title": "Hello world", "comments": [] }
    ]
  }
}

Calling it from your app

There is no dedicated GraphQL client generated alongside the REST SDKs yet - call the endpoint with any standard GraphQL client (Apollo Client, graphql-request) or a plain HTTP request, the same way you'd call any other MUDBASE endpoint:

javascript
const res = await fetch(
  `https://cloud.mudbase.dev/api/projects/${projectId}/graphql`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.MUDBASE_API_KEY,
    },
    body: JSON.stringify({
      query: `{ posts { id title comments { body } } }`,
    }),
  },
);
const { data, errors } = await res.json();
const res = await fetch(
  `https://cloud.mudbase.dev/api/projects/${projectId}/graphql`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.MUDBASE_API_KEY,
    },
    body: JSON.stringify({
      query: `{ posts { id title comments { body } } }`,
    }),
  },
);
const { data, errors } = await res.json();
Chat with us