Configuration
Compose the application, admin, localization, collections, globals, plugins, and runtime services.
Your content model and server behaviour begin as a typed ridu.Config value. Ridu resolves it into
the schema used by the database, REST API, generated Go and TypeScript contracts, and the admin.
Projects put the composition function in content/config.go by default, but you may split config
across any number of Go files.
Application config
At minimum, a config needs a non-empty application name and one collection:
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
)
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Collections: []ridu.Collection{
{
Slug: "posts",
Fields: []field.Definition{
field.Text("title", field.Required()),
field.Textarea("summary"),
},
},
},
}
}Because config is Go, the compiler catches misspelled fields, wrong callback signatures, and field options used with incompatible constructors. Resolution then handles rules that require the whole application, such as duplicate slugs, missing relationship targets, invalid locale graphs, and plugin compatibility.
Config options
| Option | Required | What it controls |
|---|---|---|
Name |
Yes | The author-facing application name shown by Ridu tooling and the admin. Whitespace is trimmed and an empty name is rejected. |
Collections |
Yes | Repeatable document models. At least one collection is required. |
Globals |
No | Singleton documents such as site settings or navigation. |
Endpoints |
No | Compiled root custom endpoints below /api; handlers are anonymous by default. |
Admin |
When auth is enabled | Admin.User selects which auth-enabled collection may enter the admin. |
Localization |
No | Content locales, the default locale, ordered fallbacks, right-to-left metadata, and request-visible locales. The zero value disables content localization. |
Hooks |
No | Application-wide failure hooks. Collection and global lifecycle hooks live on the resource they affect. |
Plugins |
No | Compiled extensions, applied in declaration order. |
AfterCommit |
No | A dispatcher for collection and global hooks that run after a successful transaction. |
Storage |
When uploads are enabled | The object backend for upload bytes. It can instead be supplied lazily with ridu.WithUploadStorage. |
StorageNamespace |
When uploads are enabled | A stable application-owned prefix for objects in a shared backend. |
See the ridu.Config reference for every field.
How Ridu reads config
Ridu does not parse Go files looking for collections. The project entry calls your Config()
function, so access rules, hooks, computed fields, and plugin constructors execute as Go code.
The CLI finds that entry through ridu.toml:
version = 1
database = "postgres" # or "sqlite" or "mongodb"; credentials remain runtime-owned
package_manager = "npm" # or "bun", "pnpm", or "yarn"
entry = "./cmd/server"
admin = "./admin"
schema = "./generated/ridu.schema.json"
client = "./generated/ridu.generated.ts"
openapi = "./generated/ridu.openapi.json"
# Optional after enabling the GraphQL plugin:
# generated.graphql.schema = "./generated/ridu.graphql"
migrations = "./migrations"
assets = "./internal/adminassets/dist"
plugins = "./ridu.plugins.json"
plugin_go = "./content/ridu_plugins.generated.go"ridu generate, ridu dev, and migration commands run the configured entry to resolve the current
schema. Plugin transforms run in declaration order before Ridu validates the application and creates
the schema manifest. Hooks, access rules, computed resolvers, storage, and endpoint handlers remain
in the Go runtime.
Change content/*.go rather than generated JSON. The development loop regenerates the manifest and
other derived files.
Compose a larger application
As the application grows, declare static collections and globals as focused package variables. Their filenames and package layout are yours to choose.
package content
import "github.com/riducms/ridu"
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Admin: ridu.AdminConfig{User: "users"},
Plugins: installedPlugins(),
Collections: []ridu.Collection{Users, Posts, Media},
Globals: []ridu.Global{SiteSettings},
}
}For example, the Posts collection can live beside it with its imports and behaviour close to the
model it belongs to:
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
"github.com/riducms/ridu/plugins/richtext"
)
var Posts = ridu.Collection{
Slug: "posts",
Admin: ridu.CollectionAdmin{
UseAsTitle: "title",
DefaultColumns: []string{"title", "status"},
Group: "Editorial",
},
Fields: []field.Definition{
field.Text("title", field.Required()),
field.Select(
"status",
field.OneOf("draft", "published"),
field.Default("draft"),
),
richtext.Field("content"),
},
}Ridu defensively copies these declarations before plugins transform or validate the configuration. Treat exported collection variables as definitions rather than shared application state: use a function instead when construction needs options, runtime values, fallible setup, or a fresh caller-owned mutable value.
ridu add and ridu plugin remove update installedPlugins(), Go dependencies, and matching admin
imports together. Add application-local plugins directly to Config.Plugins.
See Collections and globals for capabilities and lifecycle behaviour, and Fields for the complete field vocabulary.
Configure the admin identity
Config.Admin selects which auth collection owns admin sessions. It does not enable authentication
by itself. Set Auth: true on the collection, then name that collection in Admin.User:
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
)
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Admin: ridu.AdminConfig{User: "users"},
Collections: []ridu.Collection{
{
Slug: "users",
Auth: true,
Fields: []field.Definition{
field.Text("email", field.Required(), field.Unique()),
},
},
Posts,
},
}
}If several collections enable auth, only the selected collection gains admin access. Ridu rejects
an absent, unknown, or non-auth-enabled Admin.User whenever any auth collection exists. An
application without auth collections may leave Admin at its zero value.
Collection list columns, labels, groups, descriptions, and live preview belong to
ridu.CollectionAdmin; global presentation belongs to ridu.GlobalAdmin. Those values shape the
admin but do not grant authorization. See Admin and
Authentication for those separate concerns.
Admin interface language is separate from content locale. Configure Admin.Localization with the
allowed BCP-47 languages, default language, browser-safe IANA timezones (or fixed offsets), and the
default timezone. English, French, and Arabic catalogs ship with Ridu. Editors can persist their
choice from the account screen; the whole interface, accessibility text, plural rules, formatting,
and right-to-left layout update together. Application, resource, field, choice, relationship,
language, and timezone labels can provide LabelTranslations maps without changing stable IDs.
Add content localization
Localization is opt-in. Once it is configured, mark individual fields with field.Localized();
ordinary fields continue to store one shared value.
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
"github.com/riducms/ridu/schema"
)
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Localization: ridu.LocalizationConfig{
DefaultLocale: "en",
Locales: []ridu.Locale{
{Code: "en", Label: "English"},
{
Code: "fr",
Label: "Français",
FallbackLocales: []schema.LocaleCode{"en"},
},
{
Code: "ar",
Label: "العربية",
RTL: true,
FallbackLocales: []schema.LocaleCode{"en"},
},
},
},
Collections: []ridu.Collection{
{
Slug: "posts",
Fields: []field.Definition{
field.Text("title", field.Required(), field.Localized()),
field.Text("slug", field.Required(), field.Unique()),
},
},
},
}
}Every configured locale needs a unique valid code and non-empty label. DefaultLocale must name
one of them. Fallbacks are enabled by default and follow each locale’s ordered
FallbackLocales; unknown locales, self-fallbacks, duplicates, and cycles fail resolution. Set
DisableFallback: true when reads must return only the exact requested locale.
Localization.AvailableLocales can reduce the locales shown to a particular admin request using
its actor and local API. It is executable request policy, so it is not serialized into the public
manifest and does not weaken locale validation or authorization. See the localization options in
the schema reference.
Register plugins
Plugins are compiled Go values. Ridu checks compatibility and applies config transforms in declaration order. Runtime callbacks and secrets remain in the binary.
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/plugins/graphql"
"github.com/riducms/ridu/plugins/richtext"
)
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Plugins: []ridu.Plugin{
richtext.New(),
graphql.New(graphql.Options{MaxDepth: 10}),
},
Collections: []ridu.Collection{Users, Posts},
}
}When a plugin contributes an admin package, generation emits a static import and verifies its key, API version, and pairing version against the backend descriptor. Production never downloads or installs plugin code dynamically. See Plugins for installation, removal, and authoring.
Configure root hooks and after-commit dispatch
Config.Hooks contains application-wide failure observation. Put operation lifecycle behaviour on
the collection or global that owns it; those resource hooks have the document, original values,
actor, locale, and local API in their context.
package content
import (
"log/slog"
"github.com/riducms/ridu"
"github.com/riducms/ridu/core"
)
func reportRootFailure(ctx ridu.HookContext) error {
slog.Error(
"ridu operation failed",
"operation", ctx.Operation,
"error", ctx.Error,
)
return nil
}
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
Hooks: core.RootHooks{
AfterError: []ridu.Hook{reportRootFailure},
},
Collections: []ridu.Collection{Users, Posts},
}
}Resource AfterCommit hooks run only after the document transaction succeeds. Without a custom
dispatcher Ridu runs them immediately. Set Config.AfterCommit to an implementation of
ridu.AfterCommitDispatcher when the application needs one place to schedule, instrument, or
otherwise control committed effects. The dispatcher receives operation and resource identity plus
the callback to run; an error at this point cannot roll the committed document back.
See Hooks for the full lifecycle and transaction boundaries.
Configure upload storage
Upload collections store document metadata in the document-store adapter and file bytes in an
object-storage adapter. Every upload-enabled application also needs a stable StorageNamespace;
do not derive it from Config.Name, because the display name can change.
For runtime-created clients, prefer ridu.WithUploadStorage. Its factory is evaluated only when
the server starts, not while the CLI resolves config:
package main
import (
"context"
"log"
"os"
"example.com/acme/content"
"github.com/riducms/ridu"
"github.com/riducms/ridu/adapters/postgres"
"github.com/riducms/ridu/adapters/storage/s3"
"github.com/riducms/ridu/storage"
"github.com/riducms/ridu/store"
)
func main() {
err := ridu.Execute(
content.Config(),
ridu.WithStore(func(ctx context.Context) (store.Store, error) {
return postgres.Open(ctx, os.Getenv("DATABASE_URL"))
}),
ridu.WithAddress(":8080"),
ridu.WithUploadStorage(func(context.Context) (storage.Backend, error) {
return s3.New(s3.Config{
Endpoint: os.Getenv("S3_ENDPOINT"),
Region: os.Getenv("S3_REGION"),
Bucket: os.Getenv("S3_BUCKET"),
AccessKey: os.Getenv("S3_ACCESS_KEY"),
SecretKey: os.Getenv("S3_SECRET_KEY"),
})
}),
)
if err != nil {
log.Fatal(err)
}
}The content config still declares the namespace and upload collection:
package content
import "github.com/riducms/ridu"
func Config() ridu.Config {
return ridu.Config{
Name: "Acme Editorial",
StorageNamespace: "acme-content",
Collections: []ridu.Collection{Users, Posts, Media},
}
}ridu.Resolve validates the serializable upload schema. Runtime construction additionally checks
that upload storage is present and that the document store supports every enabled capability. See
Uploads and Object storage for backend and privacy options.
Server entry
ridu.Execute joins config to runtime services. A PostgreSQL server entry looks like this:
package main
import (
"context"
"log"
"os"
"example.com/acme/content"
"example.com/acme/internal/adminassets"
"github.com/riducms/ridu"
"github.com/riducms/ridu/adapters/postgres"
"github.com/riducms/ridu/store"
)
func main() {
err := ridu.Execute(
content.Config(),
ridu.WithStore(func(ctx context.Context) (store.Store, error) {
return postgres.Open(ctx, os.Getenv("DATABASE_URL"))
}),
ridu.WithAddress(":8080"),
ridu.WithHandlerOptions(ridu.HandlerOptions{
AdminAssets: adminassets.FS(),
AllowedOrigins: []string{
"https://www.example.com",
},
}),
)
if err != nil {
log.Fatal(err)
}
}SQLite-selected projects import adapters/sqlite and open RIDU_SQLITE_PATH; see
SQLite. MongoDB-selected projects import adapters/mongodb, open DATABASE_URL,
and verify the resolved index plan before serving; see MongoDB.
Use ridu.New(config, store) for tests, embedded use, or a custom HTTP process. It still applies
access, validation, and hooks. Build production applications with ridu build; a direct go build
omits the migration-history fingerprint, so /readyz fails with an official adapter.
Validate before runtime
Use ridu.Resolve in focused tests to validate config and inspect the immutable manifest without
opening a database:
package content
import (
"errors"
"testing"
"github.com/riducms/ridu"
"github.com/riducms/ridu/schema"
)
func TestConfigResolves(t *testing.T) {
manifest, err := ridu.Resolve(Config())
if err != nil {
var validation *schema.ValidationError
if errors.As(err, &validation) {
for _, issue := range validation.Issues {
t.Logf("%s [%s]: %s", issue.Path, issue.Code, issue.Message)
}
}
t.Fatal(err)
}
if manifest.Snapshot().Application.Name != "Acme Editorial" {
t.Fatal("resolved the wrong application config")
}
}Schema validation errors carry a stable Code, exact Path, and actionable Message. One pass
can report several independent issues, which is more useful than fixing a large config one failure
at a time. Runtime-only capability checks—such as requiring store.AuthStore for auth collections
or object storage for uploads—run when the application is bound to its backends.
Develop and review generated contracts
While ridu dev is running, saving a config change automatically resolves it, regenerates every
derived contract, synchronizes safe additive database changes, and restarts the application. Before
committing, verify that the generated files are current:
npm run ridu -- generate --check
npm run ridu -- checkIf the development loop is not running, npm run ridu -- generate performs the same generation as
a one-shot command. It resolves the Go config once, then atomically updates the canonical manifest,
OpenAPI document, generated Go models and handles, application-bound TypeScript client, and static
admin plugin registry. ridu check fails when committed generated output has drifted.
content/*.go
↓ Config()
ridu.Resolve + plugin transforms
↓ immutable manifest
schema · OpenAPI · Go types · TypeScript SDK · admin formsFor a database change, generation describes the desired application but does not silently define a production migration. Create and review an immutable migration artifact separately:
npm run ridu -- migrate create --name add-post-summary
npm run ridu -- migrate verifyApply that reviewed artifact with migrate up during deployment. ridu dev handles local schema
sync.
See Ridu CLI for the development and release commands.