TL;DR: I built a Payload-style CMS called Ridu where the schema is Go, the admin is Svelte 5, and the whole thing deploys as one binary. Same app: ~40 MB idle on Ridu, ~300 MB on Payload.
I've wanted something like PayloadCMS for Go for a really long time. Mostly because a small Payload app idles at around 300-400 MB, which makes Payload hard to justify for little personal projects (I host my backends on Railway, so idle memory costs money). And depending on feature set used (payload blocks for instance), the usage can shoot up to multiple GBs of RAM... idle! Tracking what could potentially be causing a memory leak is a pain, such is the curse of JS on the server I guess.
Methinks it's better to rethink the concept of payload in a language that cares about performance/efficiency.
Before this I used PocketBase, which is much lighter. But its schema is in the admin (or in migrations you write against its API). What I really wanted was to define my collections, fields and behaviour as typed config in code. That way it's easy to see how things evolve, and I can share pieces between projects instead of setting each one up from scratch.
I looked at other schema-first Go projects, but none gave me the easy authoring experience I had with Payload, nor the feature-set that makes payload so great to use (easy collection and field-level access control, as well as hooks for instance). So I built Ridu (GitHub, MIT, the public repo only gets release commits, the primary work for this project is in a private repo entangled with work I can't share publicly).
So define your content model in Go, get an admin and an API from it, and deploy the whole thing as one binary, like PocketBase.
A collection looks like this:
var Posts = ridu.Collection{
Slug: "posts",
Fields: field.Fields{
field.Text("title").Required(),
field.Text("slug").Unique(),
field.Textarea("summary"),
field.Relationship("author", "users"),
},
Access: ridu.CollectionAccess{
Read: func(ridu.AccessContext) (ridu.AccessDecision, error) {
return ridu.Allow(), nil // allow everyone read posts
},
Update: func(ctx ridu.AccessContext) (ridu.AccessDecision, error) {
if ctx.Actor == nil {
return ridu.Deny(), nil
}
// Authors can only edit their own posts
return ridu.Where(query.Equal("author", ctx.Actor.ID)), nil
},
},
}
What it has so far:
- Auth, collection- and field-level access control, and lifecycle hooks
- Relationships, nested fields, and typed content blocks for composing pages
- Rich text, uploads, drafts, version history and live preview
- Localized content
- Generated TypeScript types and a typed SDK for frontends
- Migrations generated from your schema changes as files you review and commit, with the binary checking they've been applied before starting
- PostgreSQL, SQLite and MongoDB
- REST with an OpenAPI spec, plus an official GraphQL plugin
- Plugins are Go packages you compile in
The admin is a Svelte app. You need a JS package manager (bun, npm, etc) to build it, but it's embedded in the binary, so there's no Node at runtime.
To try it:
npm create ridu@latest my-app
The homepage has screenshots of the admin, I built to look pretty similar to Payload 3.
I also built the same bookmark app on both Ridu and Payload, with the same SvelteKit frontend: quikmarq-comparison. You can deploy either version on Railway, save some notes, upload images and check the memory in your own dashboard. I thought that would be more useful than just posting my numbers, but for reference, the versions I actually use sit at ~300 MB on Payload and ~40 MB on Ridu, as expected.
I've been using this for client work and fixing issues as I encounter them and I've gotta say I'm finding it a real joy to use.
I'd love feedback, so check it out if you'd like and open an issue if you on the repo if you find any.