GraphQL
ExperimentalAdd 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:
ridu add graphql \
--go-package github.com/riducms/ridu/plugins/graphql \
--admin-package @riducms/plugin-graphqlThe 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 tidybun add --cwd admin @riducms/plugin-graphql
go mod tidypnpm --dir admin add @riducms/plugin-graphql
go mod tidyyarn --cwd admin add @riducms/plugin-graphql
go mod tidyTo run GraphQL without the admin playground, set DisablePlayground: true; the admin package is
then not needed.
Enable the endpoint
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:
go mod tidy
npm run devridu 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, andCtrl+Click(⌘+Clickon 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:
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:
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:
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:
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:
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:
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:
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:
generated.graphql.schema = "./generated/ridu.graphql"The development loop now writes the exact compiled schema without enabling network introspection:
npm run devUse 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.
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.