Browse all documentation

Writing data

Create, update, duplicate, and delete documents and globals from Go, TypeScript, and REST, and keep concurrent edits from overwriting each other.

Writes go through the same engine as reads, whichever API you use: access rules, validation and hooks run every time, and the response is the saved document. Examples use the example schema and the same tab setup as Querying data.

Create

A new post:

Go
post, err := local.Create(ctx, "posts", store.Values{
  "title": store.String("Draft idea"),
  "tags":  store.List(store.String("idea")),
}, ridu.MutationOptions{Actor: user})
TypeScript
const post = await ridu.create('posts', {
  title: 'Draft idea',
  tags: ['idea']
});
REST
curl -X POST "$RIDU/api/collections/posts" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Draft idea","tags":["idea"]}'
Result
{
  "id": "posts_4c1a7d85841d75a1d744f90f",
  "title": "Draft idea",
  "status": "draft",
  "tags": ["idea"],
  "createdAt": "2026-10-06T13:48:20.726903Z",
  "updatedAt": "2026-10-06T13:48:20.726903Z"
}

Fields you leave out get their defaults, so status is draft. Over REST the document arrives as { "doc": { … } }; Go and TypeScript return the document itself. In Go, build values with store.String, store.Number, store.List and the other value constructors, or use typed handles.

Update

Send only the fields you want to change. The rest keep their stored values.

Retitle a post:

Go
post, err := local.Update(ctx, "posts", postID, store.Values{
  "title": store.String("A better idea"),
}, ridu.MutationOptions{Actor: user})
TypeScript
const post = await ridu.update('posts', postID, {
  title: 'A better idea'
});
REST
curl -X PATCH "$RIDU/api/collections/posts/$POST_ID" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"A better idea"}'

To clear a field, send null (store.Null() in Go).

Update a versioned document

A collection with Versions: true keeps every saved revision, and a published document only changes by being published again. Send your changes to the publish operation, with the revision you last read:

Go
// page is the document you read before editing.
updated, err := local.PublishChanges(ctx, "pages", page.ID,
  store.Values{"title": store.String("About us")},
  ridu.MutationOptions{
    ExpectedRevision: page.Revision,
    Actor:            user,
  },
)
TypeScript
const updated = await ridu.publishChanges(
  'pages',
  page.id,
  { title: 'About us' },
  { revision: page._revision }
);
REST
curl -X POST "$RIDU/api/collections/pages/$PAGE_ID/publish" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "1"' \
  -d '{"title":"About us"}'

A plain update of a published document is refused with publish_required (409), and the message names the publish route. With drafts enabled, you can also save changes as a draft first; see Drafts and versions.

Don’t overwrite someone else’s edit

The revision makes the write conditional. If someone saved the document after you read it, its revision has moved on and your write fails with conflict (409) instead of replacing their changes:

REST response
{
  "error": {
    "code": "conflict",
    "status": 409,
    "message": "document conflicts with an existing value"
  }
}

Reload the document, show the newer version, and let the author decide. Pass the revision on every interactive edit; document locks coordinate editors but don’t replace it. Leaving it out, or passing zero in Go, writes without the check.

Delete

Go
_, err := local.Delete(ctx, "posts", postID, ridu.MutationOptions{
  Actor: user,
})
TypeScript
await ridu.delete('posts', postID);
REST
curl -X DELETE "$RIDU/api/collections/posts/$POST_ID" \
  -H "Authorization: Session $TOKEN"
REST response
{ "id": "posts_0354693e1c584b9c537d890e", "deleted": true }

In a collection with trash, delete moves the document to the trash instead; restore and permanent delete are separate operations. See Bulk actions and trash.

Duplicate

Copy a document, optionally changing some fields:

Go
copied, err := local.Duplicate(ctx, "posts", postID, store.Values{
  "title": store.String("Copy of a better idea"),
}, ridu.MutationOptions{Actor: user})
TypeScript
const copied = await ridu.duplicate('posts', postID, {
  title: 'Copy of a better idea'
});
REST
curl -X POST "$RIDU/api/collections/posts/$POST_ID/duplicate" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Copy of a better idea"}'

The copy is a new document with its own ID. Access rules and hooks run as for a create.

Update a global

A global has no create or delete: updating it the first time saves it.

Go
settings, err := local.UpdateGlobal(ctx, "site-settings",
  store.Values{"siteName": store.String("Acme")},
  ridu.MutationOptions{Actor: user},
)
TypeScript
const settings = await ridu.updateGlobal('site-settings', {
  siteName: 'Acme'
});
REST
curl -X PATCH "$RIDU/api/globals/site-settings" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"siteName":"Acme"}'

Before its first save, a global reads as its defaults. A versioned global changes through PublishGlobalChanges (publishGlobalChanges in TypeScript), like a versioned document.

Change several documents at once

Set two posts back to draft:

Go
ids := []string{firstID, secondID}
posts, err := local.BulkUpdate(ctx, "posts", ids,
  store.Values{"status": store.String("draft")},
  ridu.BulkOptions{Actor: user},
)
TypeScript
const posts = await ridu.bulkUpdate('posts', [firstID, secondID], {
  status: 'draft'
});
REST
curl -X POST "$RIDU/api/collections/posts/bulk" \
  -H "Authorization: Session $TOKEN" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{"action":"update","ids":["$FIRST_ID","$SECOND_ID"],
 "data":{"status":"draft"}}
JSON

A bulk action takes 1 to 100 IDs and saves all of them or none. Each document still passes its own access rules, validation and hooks. The same form covers publishing, unpublishing, deleting, restoring and permanently deleting several documents; see Bulk actions and trash.

When a write fails

A write that breaks a rule saves nothing and fails with a code you can branch on:

Code Status Means
validation 422 A field is invalid; issues lists each one with its path
conflict 409 A stale revision, or a unique value already in use
publish_required 409 A plain update of a published versioned document; publish the changes instead
access_denied 403 The caller may not make this change
not_found 404 The document doesn’t exist, or the caller can’t see it
rejected 422 A hook refused the change; its message is written for people

Handle them in Go with *ridu.OperationError, in TypeScript with RiduError, and over REST from the error envelope.