Open source · MIT · Ridu 0.7.0

Your content model in Go.Your CMS in one binary.

Collections, access rules, hooks, and a polished admin, defined in Go config you can review and test. Ship the API, admin, and jobs as a single binary, with no Node in production.

Start with Ridu
npm create ridu@latest
The Ridu admin editing a journal post, with the live preview of the designed journal page beside the form.
Edit on the left, see your site update on the right. The admin comes from your Go config.

How it works

From a Go struct to a running CMS.

Pick a step, or let it play.
content/posts.go
var Posts = ridu.Collection{
  Slug: "posts",
  Fields: field.Fields{
    field.Text("title").Required(),
    field.Textarea("summary").MaxLength(280),
    field.Select("status", "draft", "published"),
  },
}

You get

  • →An admin list and edit form
  • →REST endpoints and an OpenAPI spec
  • →A typed TypeScript SDK
  • →A reviewed database migration
The generated Ridu admin form for a post, with title, status, author, and content.
app/posts.ts
const page = await ridu.list('posts', {
  where: { status: { equals: 'published' } }
});
response · 200
{
  "docs": [
    {
      "id": "posts_7514213d859111e0",
      "title": "Hello from Ridu",
      "status": "published"
    }
  ],
  "totalDocs": 1
}
terminal
$ npm run build
[ridu] Verified migration artifact history
Built dist/app

$ ls -lh dist/app
-rwxr-xr-x  1 you  staff   28M  dist/app

Behaviour lives next to the data

A few lines of Go. A visible result.

The admin, REST, the SDK, and your Go code all run through the same rules, so what you write here is what every caller gets.

Access control

Filter reads inside the database query.

Collection.Access
Read: func(ridu.AccessContext) (
  ridu.AccessDecision, error,
) {
  return ridu.Where(
    query.Equal("status", "published"),
  ), nil
},

Signed-out visitor sees

  • Hello from RiduPublished
  • Designing with blocksPublished
  • Shipping one binaryPublished

Drafts never appear, not even in the total count.

Lifecycle hooks

Clean data before every save, from any caller.

BeforeChange
func(ctx ridu.HookContext) error {
  title, _ := ctx.Data["title"].StringValue()
  ctx.Data["title"] = store.String(
    strings.TrimSpace(title),
  )
  return nil
}

Saved title

"··Hello, Ridu···"→"Hello, Ridu"

Computed fields

Return a value without storing it.

Collection.Fields
field.Virtual("displayName",
  func(ctx operation.Context) (
    operation.Value[string], error,
  ) {
    first, _ := ctx.Root.String("firstName")
    last, _ := ctx.Root.String("lastName")
    name := first + " " + last
    return operation.Present(name), nil
  })

GET /api/people/ada

{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "displayName": "Ada Lovelace"
}

Typed content blocks

Let editors compose pages from parts you define.

Collection.Fields
field.Blocks("layout",
  field.Block{Slug: "hero", Fields: heroFields},
  field.Block{Slug: "quote", Fields: quoteFields},
  field.Block{Slug: "gallery", Fields: galleryFields},
)

Editors choose

Hero
Quote
Gallery

Deploy

Production is one file.

ridu build compiles the API, the admin, background jobs, and migration checks into a single executable. The image around it needs nothing else.

Database

Dockerfile
  1. FROM gcr.io/distroless/static-debian12No Node, no npm, not even a shell
  2. COPY dist/my-app /appThe API, admin, and background jobs
  3. ENV DATABASE_URL=postgres://…Your PostgreSQL connection URL
  4. EXPOSE 8080Health checks answer at /readyz
  5. ENTRYPOINT ["/app"]Refuses to start on an unmigrated database

Coming from Payload?

The model you know, written in Go.

Collections, fields, access rules, hooks, versions, and live preview carry over. The migration guide maps each Payload concept to its Ridu equivalent, side by side.

Read the migration guide
Payload · Posts.ts
fields: [
  { name: 'title', type: 'text', required: true },
  { name: 'slug', type: 'text', unique: true },
  { name: 'summary', type: 'textarea' },
  { name: 'author', type: 'relationship',
    relationTo: 'users' },
],
Ridu · posts.go
Fields: field.Fields{
  field.Text("title").Required(),
  field.Text("slug").Unique(),
  field.Textarea("summary"),
  field.Relationship("author", "users"),
},

Running in five minutes.

Pick the starter and SQLite: no Docker, no database service. Create a post in the admin, then read it from the generated SDK.

npm create ridu@latest my-app