Open source · MIT · Ridu 0.25.0

Headless CMS and app framework for Go

Define collections, fields, access rules and hooks in Go. Ridu generates an admin, a REST API, database migrations and TypeScript types from that config, and builds the whole thing into one binary.

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.
The edit form is generated from the collection's Go config. The page on the right is a separate frontend, updating through live preview as you type.

How it works

How a Ridu project fits together

The steps cycle on their own. Select one to stay on it.
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"),
  },
}

Generated from this file

  • →An admin list view and edit form
  • →REST endpoints and an OpenAPI spec
  • →TypeScript types and a typed SDK
  • →A database migration for you to review
  • →A GraphQL API, if you add the plugin
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 in code

Access rules, hooks and computed fields are Go functions

They run for every request, whether it comes from the admin, the REST API, the SDK or your own Go code. Here are four small examples and what each one does.

Signed-out visitors only get published posts. The filter is applied in the database query.

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

What a signed-out visitor gets

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

Drafts aren't returned, and they aren't included in the total count.

Trims the title before it's saved, whichever API the save came from.

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"

Add a value to API responses 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"
}

Define the blocks editors can add to a page, and the fields in each one.

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

In the admin

Hero
Quote
Gallery

Deploy

Deploy with a five-line Dockerfile

ridu build produces one executable with the API, the admin and background jobs in it. Nothing else runs in production, so the image only needs that file. Migrations are generated from your config, and the binary checks they have been applied before it starts. There's also aRailway guide.

Database

Dockerfile
  1. FROM gcr.io/distroless/static-debian12A static base image with no shell
  2. COPY dist/my-app /appYour app, with the admin built in
  3. ENV DATABASE_URL=postgres://…Your PostgreSQL connection URL
  4. EXPOSE 8080Health checks use /readyz
  5. ENTRYPOINT ["/app"]Checks migrations are applied first

Coming from Payload?

Most Payload concepts carry over

Collections, fields, access rules, hooks, drafts, versions and live preview work the way you'd expect. The migration guide goes through each Payload concept and shows the Ridu version next to it.

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"),
},

Try it locally

Pick the starter template and SQLite and you won't need Docker or a database server. The quickstart walks through creating a post in the admin and reading it back with the generated SDK.

npm create ridu@latest my-app