Browse all documentation

Add an optional manifest-derived GraphQL endpoint with typed resources, shared authorization, bounded execution, and an admin playground.

GraphQL is an optional compiled transport. REST-only applications do not import it and therefore do not link the parser, executable schema, handler, or GraphQL runtime into their binary.

The plugin derives one immutable executable schema from the canonical Ridu manifest at startup. Resolvers enter the same local operation engine as REST and the SDK; GraphQL selection does not bypass access, validation, hooks, transactions, localization, population, or redaction.

Use it in a new project

Create an ordinary starter or blank project, then follow the existing-project steps below. Applications that do not install the plugin do not link its parser or execution runtime into the binary, and their admin does not download the playground.

Add it to an existing project

Add the Go plugin and its admin package together:

terminal
ridu add graphql \
  --go-package github.com/riducms/ridu/plugins/graphql \
  --admin-package @riducms/plugin-graphql

The command installs @riducms/plugin-graphql in the admin workspace and registers graphqlplugin.New() in the generated plugin list, so installedPlugins() already includes it. Register it by hand instead, as below, when you need options. The generated admin registry imports the playground; do not register it again in admin.config.ts.

If you register the plugin by hand, install the admin package yourself:

npm install --workspace admin @riducms/plugin-graphql
go mod tidy

To run GraphQL without the admin playground, set DisablePlayground: true; the admin package is then not needed.

Enable the endpoint

content/config.go
package content

import (
  "github.com/riducms/ridu"
  graphqlplugin "github.com/riducms/ridu/plugins/graphql"
)

func Config() ridu.Config {
  return ridu.Config{
    Name: "Acme Editorial",
    Plugins: []ridu.Plugin{
      graphqlplugin.New(),
    },
    Collections: []ridu.Collection{Users, Posts},
  }
}

The default route is POST /api/graphql. An invalid GraphQL name, generated type/root collision, unknown resource override, or malformed extension stops startup instead of hiding part of the schema. Signed-in admins can explore and run operations in the admin playground.

Start the complete development loop:

terminal
go mod tidy
npm run dev

ridu dev regenerates the committed SDL and other contracts before starting the API and admin. Enabling only the transport does not add stored fields or adapter tables, so it does not need a database migration. If the same change also modifies resources, prepare the selected adapter’s reviewed migration before deployment. Run ridu check before committing so generated SDL drift is caught.

With the server running, open GraphQL in the admin sidebar and run the starter query. Also confirm that an unauthorized query receives the same denial and field redaction as REST.

Explore the schema in the playground

The admin includes a GraphQL playground at /admin/graphql, like Payload’s GraphQL playground. It runs operations against your endpoint with the current admin credentials, when present, so every result follows the same access rules as the rest of the admin.

  • Query editor: schema-aware autocompletion (Ctrl+Space), inline validation, and Ctrl+Click (⌘+Click on a Mac) on a field or type to open its docs.
  • Run: Ctrl+Enter (⌘+Enter) or the Run button. When the editor holds several named operations, the one under the cursor runs.
  • Variables: one JSON object sent with the operation.
  • Response: the status, duration, and the ordinary GraphQL response, including errors.
  • Docs: browse root types, follow type links, and search type and field names.

The playground keeps your last query and variables in the browser and restores them on your next visit. Its first query lists a few documents from your first collection.

The schema reaches the playground through an admin loader and follows the admin’s existing access policy. An application with admin authentication therefore requires an admin session; an intentionally public no-auth admin exposes the same schema metadata its manifest already exposes. This does not enable GraphQL endpoint introspection: AllowIntrospection remains off by default. The playground follows a custom Path, so it always targets the endpoint the plugin serves.

To serve GraphQL without the playground, disable it:

go
graphqlplugin.New(graphqlplugin.Options{
  DisablePlayground: true,
})

This removes the admin route and loader. Remove @riducms/plugin-graphql from the admin workspace afterwards.

Query collections

Root names come from resolved singular/plural labels. A posts collection normally produces Post, Posts, and countPosts queries:

graphql
query RecentPosts {
  Posts(
    where: { featured: { equals: true }, score: { greater_than: 5 } }
    sort: ["-score"]
    page: 1
    limit: 20
  ) {
    docs {
      id
      title
      category {
        id
        name
      }
    }
    totalDocs
    totalPages
    hasNextPage
  }
}

totalDocs and totalPages are counted by default. Pass pagination: false to skip the count when you only need hasNextPage, nextPage, and prevPage; both totals are then null, and the other fields stay exact (see Paginate and count). A join field counts only when it receives count: true. Otherwise its totalDocs is null and hasNextPage is still exact.

Typed filter inputs compile into Ridu’s finite query vocabulary. Filters nest like the schema: a group or array field takes an input of its own fields, such as seo: { description: { contains: "launch" } }, and a blocks field takes an input keyed by block slug, such as layout: { hero: { heading: { equals: "Welcome" } } }. Selected relationship/upload fields become bounded operation-engine population and reapply target access and redaction.

Groups and arrays have typed nested output/input objects. Select/radio choices use GraphQL enums. Blocks return typed output unions selected with inline fragments; mutation input uses JSON because GraphQL has no input union. Polymorphic references use { relationTo, value }.

Write a JSON input as a literal or pass the whole value as one variable, such as layout: $layout. Ridu rejects a variable nested inside a JSON literal, such as layout: [{ caption: $caption }], because that value cannot be applied.

A block registered in Config.Blocks has one output type and one filter input, named from its type name, wherever it is referenced: ... on HeroBlock selects a hero block in every collection and nested block, and HeroBlockWhere filters it. Filters share one operator input per value type, such as RiduStringWhere and RiduNumberWhere, and select filters reuse the field’s output enum. The schema therefore grows with block definitions rather than with every place a block is used.

Lists, multi-selects, has-many relationships and uploads, and polymorphic relationships are filtered by membership with in, not_in and exists. A polymorphic candidate uses the relationship’s GraphQL shape: subjects: { in: [{ relationTo: POSTS, value: "post_123" }] }.

Create and mutate

Collections add generated create, update, delete, and duplicate fields when mutations are enabled:

graphql
mutation CreatePost($category: ID!) {
  createPost(data: { title: "Hello", category: $category }) {
    id
    title
    category {
      id
      name
    }
  }
}

Optimistic mutations accept expectedRevision when the resource is versioned. Duplicate runs the ordinary create lifecycle. Upload-enabled collections deliberately omit GraphQL create: JSON cannot carry the byte source or server-owned metadata. Use multipart/remote upload APIs for new files; upload document reads and metadata remain available in GraphQL.

Globals, drafts, versions, and trash

Globals expose read/update and matching version/draft operations. Versioned collections add single/list version queries and restore; draft-enabled resources add publish/unpublish. Trash adds trash: true reads, restore-deleted, and permanent-delete mutations. draft: true includes or writes a draft; draft: false selects published behavior.

These operations use the same revision checks, access rules, hooks, retention, reference cleanup, and scheduled-state semantics as other transports. Scheduling, document locks, previews, bulk operations, and raw file transfer remain REST/SDK-focused surfaces rather than inferred GraphQL fields.

Localized content

Configured locales become a deterministic RiduLocale enum:

graphql
query LocalizedPost($id: ID!) {
  english: Post(id: $id, locale: EN) {
    title
  }
  french: Post(id: $id, locale: FR, fallbackLocale: [EN]) {
    title
  }
  exactArabic: Post(id: $id, locale: AR, disableFallback: true) {
    title
  }
}

The selected locale reaches filters, relationships, access, hooks, and versions. GraphQL does not overload scalar fields with the local/REST all locale-keyed shape; request one locale per field alias when a query needs several translations.

Authentication and preferences

Auth collections generate login, current-user/session, refresh, logout, initialized-state, unlock, recovery, and verification fields according to configuration. The result contains an opaque token, expiry, exact auth collection, and redacted user.

Browser applications should prefer the REST/SDK HTTP-only cookie. A non-browser GraphQL client can authenticate with:

text
Authorization: Session <opaque-session-token>
Authorization: Bearer ridu_<id>_<api-key-secret>

Session and API-key identity includes its auth collection, so colliding document IDs across auth collections remain distinct. Admin-identity GraphQL clients also get preference read/set/delete/ reset fields with the same exact ownership contract.

Configure resources

Rename or suppress a generated GraphQL surface without changing the manifest slug, REST path, or SDK type:

go
graphqlplugin.New(graphqlplugin.Options{
  Resources: map[string]graphqlplugin.ResourceOptions{
    "posts": {
      SingularName:     "Article",
      PluralName:       "Articles",
      DisableMutations: true,
    },
  },
})

Disabling a GraphQL query/mutation removes only that transport field. It is not an access rule for REST, the local API, SDK, admin, or plugin endpoints.

Add trusted root fields

Compiled extension queries/mutations receive the authenticated actor, exact auth collection, local API, and public application facade—never a store adapter:

go
graphqlplugin.New(graphqlplugin.Options{
  Queries: []graphqlplugin.ExtensionField{
    {
      Name: "postTotal",
      Type: graphql.NewNonNull(graphql.Int),
      Cost: 5,
      Resolve: func(
        input graphqlplugin.ExtensionContext,
      ) (any, error) {
        page, err := input.Local.List(
          input.Context,
          "posts",
          ridu.ListOptions{
            Page: 1, Limit: 1,
            Actor: input.Actor, ActorCollection: input.ActorCollection,
          },
        )
        if err != nil {
          return nil, err
        }
        return *page.Total, nil
      },
    },
  },
})

Using the supplied API keeps application authorization and lifecycle behavior intact. Extension code is trusted compiled code; Cost protects query budgeting, not sandboxing.

Generate deterministic SDL

Configure a project-owned destination after enabling the plugin:

ridu.toml
generated.graphql.schema = "./generated/ridu.graphql"

The development loop now writes the exact compiled schema without enabling network introspection:

bash
npm run dev

Use ridu generate --check in CI to verify that the committed SDL matches executable config. Use ridu generate only when you need a one-shot write without starting the development server.

Resource renames, disabled surfaces, and custom root fields come from the same executable plugin instance used at runtime. They are never duplicated into the serializable manifest. Ridu validates the project-relative path through the generic generated.<plugin>.<artifact> mapping and installs the SDL atomically beside the other contracts. graphqlplugin.GenerateSDL remains available when application-owned Go tooling needs the bytes directly.

Before removing the GraphQL plugin, remove its generated mapping and delete the now-unowned SDL file. Ridu never deletes paths that disappear from ridu.toml.

Resource safeguards

Secure zero-value defaults are:

Limit Default
HTTP body 1 MiB
Encoded variables 256 KiB
Executable depth 12
Aliases 100
Total complexity 1,000
Per-list limit 100
Introspection disabled

Fragments are expanded for list/cost analysis, nested join limits are charged, and cycles fail closed. Hard syntax/token/fragment ceilings remain even if application options are raised. Keep introspection disabled on untrusted production endpoints unless your operational policy needs it.

go
graphqlplugin.New(graphqlplugin.Options{
  AllowIntrospection: true,
  MaxComplexity:      2_000,
  MaxDepth:           16,
})

GraphQL validation failures use the normal response shape. Operation errors expose stable Ridu code, status, and validation issues extensions; unexpected internal failures do not expose a message or stack.

See the graphql plugin reference, REST API, and TypeScript SDK. Use Capability status to track the remaining conformance boundary.