Move from Payload
LimitedTranslate a Payload project into Ridu config, normalized content, and a rehearsed production cutover.
Payload and Ridu share executable configuration, collections, globals, access rules, hooks, drafts, uploads, generated types, plugins, and an admin. Ridu defines the config and server runtime in Go, generates a Fetch client for TypeScript, and serves the admin from the same binary.
Treat a migration as a model translation and a data migration—not a line-by-line conversion of Payload configuration or a copy of Payload’s database tables.
Give an agent the migration contract
New projects can include the payload-to-ridu coding-agent skill. Ask the agent to inventory the
Payload application, keep a source-to-target ledger, and migrate one complete feature at a time. It
must stop for decisions about missing semantics, credentials, production writes, or lossy
conversion.
For an existing project, install it with ridu agent install --agent codex|claude|cursor|all. After
a CLI upgrade, run ridu agent sync; it will not overwrite a managed file with local edits.
Decide whether the shape fits
Start with the workflows people rely on, not only the field list.
| Evaluate | Ridu equivalent | Migration question |
|---|---|---|
| Collections and globals | ridu.Collection and ridu.Global |
Do slugs, IDs, timestamps, and singleton semantics remain stable? |
| Fields | Constructors in field, plus plugin fields |
Which values need conversion rather than a direct JSON mapping? |
| Access callbacks | allow, deny, or filtered Where decisions |
Can every rule be expressed without depending on a Node-only service? |
| Hooks | typed Go collection, field, and global hooks | Which side effects must move, and which should become durable tasks? |
| Drafts and versions | versioned resources with optional drafts | Which revision becomes the imported document? Is old history required elsewhere? |
| Upload collections | upload documents plus a storage backend | How will bytes, checksums, metadata, and derived sizes be copied and verified? |
| Admin components | statically registered Svelte plugins | Which custom React views or fields need a Svelte replacement? |
| Local API and Payload client | local Go API and generated @riducms/sdk client |
Which callers move in-process and which remain HTTP clients? |
The normalized importer accepts collection records only. Payload globals, non-default locale values, credentials, upload bytes, and complete revision timelines require custom migration code or an archival decision.
Review the capability status before committing to a cutover. In particular, custom rich-text blocks, UI localization, resumable uploads, and some long-tail query operators have narrower contracts than their Payload counterparts.
Translate the configuration
The closest conceptual mappings are:
| Payload | Ridu |
|---|---|
buildConfig({...}) |
func Config() ridu.Config |
CollectionConfig |
ridu.Collection |
GlobalConfig |
ridu.Global |
| field object | field.* constructor plus typed options |
| access callback | ridu.AccessRule returning allow, deny, or a query predicate |
| lifecycle callback | a function in ridu.CollectionHooks or ridu.GlobalHooks |
| server plugin | a compiled Go ridu.Plugin |
| admin component/import map | a Svelte/TypeScript plugin registration |
@payloadcms/plugin-seo |
paired plugins/seo and @riducms/plugin-seo packages |
@payloadcms/plugin-form-builder |
paired plugins/formbuilder and @riducms/plugin-form-builder packages |
payload.find(...) |
app.Local().List(...) or a generated typed handle |
| generated Payload client/types | generated/ridu.generated.ts, built on @riducms/sdk |
For example, a versioned post collection becomes:
package content
import (
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
"github.com/riducms/ridu/plugins/richtext"
)
var Posts = ridu.Collection{
Slug: "posts",
Versions: true,
VersionConfig: ridu.VersionConfig{
Drafts: true,
},
Admin: ridu.CollectionAdmin{
UseAsTitle: "title",
DefaultColumns: []string{"title", "author", "updatedAt"},
},
Fields: []field.Definition{
field.Text("title", field.Required()),
field.Text("slug", field.Required(), field.Unique(), field.Index()),
field.Relationship("author", field.To("users"), field.Required()),
richtext.Field("content"),
},
Access: ridu.CollectionAccess{
Read: postReadAccess,
},
}Start ridu dev early so config resolution, generation, and the local schema stay together.
ridu check catches invalid field paths, relationships, plugin pairing, and manifest problems
before any data is moved. The configuration,
fields, access control, and hooks guides
cover the corresponding contracts in depth.
Payload endpoints map to ridu.Endpoint values on the root config, a collection, or a global.
Move the handler to compiled Go, read complete /:param segments from
EndpointContext.RouteParams, and use EndpointContext.Local for access-controlled content
operations. Ridu route parameters cover complete /:param segments, not Payload’s broader
path-to-regexp wildcard, optional, or partial-segment grammar. Encoded slash and backslash values do
not match, and declarations sharing one path shape across methods must use the same parameter names.
Collection URLs also change from Payload’s
/api/<collection-slug>/… to Ridu’s /api/collections/<collection-slug>/…; global endpoints use
/api/globals/<global-slug>/…. Like Payload, custom endpoints are anonymous unless the handler
requires an actor. See Custom endpoints for the complete route and
security contract.
SEO provides localized metadata fields and server-side generators. Form Builder provides reusable forms and submissions; your application supplies the public renderer, email transport, and payment callbacks.
Produce a normalized export
Import from a migration/payload.Export instead of reading Payload’s database tables directly, which
can vary by version and adapter.
import payloadmigration "github.com/riducms/ridu/migration/payload"
source := payloadmigration.Export{
Collections: []payloadmigration.Collection{
{
Slug: "posts",
Documents: []payloadmigration.Record{
{
ID: "post_01",
Data: json.RawMessage(`{"title":"Hello","author":"user_01"}`),
Status: store.StatusPublished,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
},
},
},
}Write a small extractor inside the Payload project so it can use that project’s exact generated types and Payload APIs. Normalize these shapes:
- preserve document IDs and UTC creation/update timestamps;
- convert relationship objects to stable target IDs;
- carry polymorphic relationships as both target collection and ID where the Ridu field requires them;
- preserve array and block row identity as non-empty, unique
_keystrings; - emit Ridu rich-text documents with
version: 1, and convert or reject nodes outside the enabled feature set; - emit a valid draft or published status;
- distinguish an absent optional value from a meaningful zero,
false, or empty string; - keep upload metadata and the corresponding object-copy manifest together.
Do not export Payload password hashes, sessions, API keys, or reset tokens as content. Provision Ridu credentials independently and use a password-reset or invitation process for users.
Assess before writing
payload.Assess compares the export with Ridu’s resolved manifest. It reports unknown collections,
missing document IDs, version selection against a non-versioned target, and a selected revision that
does not exist. Issues are sorted so repeated runs are easy to diff.
manifest, err := ridu.Resolve(content.Config())
if err != nil {
return err
}
assessment := payloadmigration.Assess(manifest, source)
log.Printf(
"collections=%d documents=%d versions=%d",
assessment.Collections,
assessment.Documents,
assessment.Versions,
)
if len(assessment.Issues) > 0 {
return fmt.Errorf("migration assessment failed: %v", assessment.Issues)
}Assessment is read-only. Run it against a production-shaped export before allocating a maintenance window. It is a structural check, not a complete data preflight: decoding, required fields, relationships, row keys, rich-text nodes, upload keys, and hook behavior are validated only when records enter the operation engine. A project migration should add its own read-only checks for those shapes before cutover.
Import through the operation engine
The normalized importer calls the local API’s migration operation. Stable IDs, status, and timestamps are preserved, while access rules, normalization, validation, hooks, transactions, relationship checks, and version logic still run.
result, err := payloadmigration.Import(
ctx,
app.Local(),
source,
migrationActor,
)
if err != nil {
return err
}
log.Printf("imported=%d", result.Imported)
log.Printf("Payload post_01 became %s", result.IDs["posts"]["post_01"])Use a dedicated actor with the permissions required for the import. A nil actor is not an
administrative bypass: collections whose access rules require a user will reject it.
Import referenced collections before their dependants. Because IDs are preserved, most references need no remapping, but validation still rejects a relationship to a document that is not present. Resolve dependency cycles in the extractor or with a custom staging pass.
Stage upload bytes before their documents
An upload row is not a complete file migration. Ridu opens the original and configured variants while admitting an imported upload document, so the target objects must already exist. For each asset:
- transform the document metadata into the target upload collection shape;
- copy the original object and named variants into an isolated target prefix;
- compare every staged size and checksum with the source manifest;
- import the upload document through the operation engine;
- exercise public and private delivery paths; and
- remove failed-run orphans only after the configured reconciliation grace period.
Do not manufacture storage keys by string concatenation. Follow the target backend’s namespace and the upload contract.
Rehearse the migration
Run the complete process more than once against a fresh target restored from production-shaped backups. Record at least:
- document counts by collection and status;
- IDs and timestamps that failed to match;
- relationship and upload referential integrity;
- checksums for stored objects;
- selected rich-text documents rendered in the frontend;
- representative localized, draft, versioned, and access-filtered reads;
- hook side effects and tasks created during import;
- total duration and the final-delta duration.
Make the migration command fail on the first rejected document and print its collection and ID. The importer returns completed mappings up to that point, but it is not a resumable whole-export transaction or a mapping UI. The safest retry target is a fresh database and object prefix.
Cut over and roll back
- Take and verify database and object-store backups.
- Put Payload into read-only mode for the final delta window.
- Run the final normalized export, assessment, upload-object staging, document import, and verification.
- Run
ridu migrate verifyand the Ridu readiness checks. - Switch application reads to the generated Ridu client behind a reversible flag.
- Canary a small cohort, watching structured operation errors, audit events, and storage health.
- Keep the Payload database and objects untouched until the rollback window closes.
Rollback means switching traffic back to the still-intact Payload deployment. Do not plan a reverse data conversion during an incident. The production, migrations, and security guides cover the surrounding operational controls.
Common migration failures
| Symptom | Likely cause | What to do |
|---|---|---|
collection ... is not present |
The export slug has no Ridu collection | Add the model or exclude and archive that source collection. |
selected revision ... is missing |
The extractor named a revision it did not export | Correct selection in the source export; do not silently fall back. |
| Relationship validation fails | Targets were omitted, renamed, or imported later | Preserve IDs and import dependencies first. |
missing_row_key or duplicate_row_key |
Payload row identity was discarded | Map row IDs to stable _key values before import. |
| Rich text is rejected | The document version or enabled-node set differs | Convert to the Ridu document contract and report lossy nodes. |
| Upload metadata imports but delivery fails | The corresponding object was not copied or namespaced correctly | Verify backend keys, object checksums, and collection storage config. |
| Access is denied | The migration actor cannot perform the create | Use a dedicated, least-privileged actor and test its rules. |
For the exact importer types and functions, see the
migration/payload API reference.