docs

Database

Runtime datasets

A runtime dataset is a collection you define on one of your own types, at runtime, with a declarative schema. The schema syncs like any other data, and every peer enforces it on apply — required fields, who may edit a field after creation, who may delete a record, server-derived creator and time stamps, and whether record ids are auto-derived or caller-supplied.

Built-in types (chat, editor, …) have statically declared datasets and refuse runtime definitions with 400 type.registered. Runtime datasets are for your types.

Why it matters. There is no server-side function to put validation in. Every device applies every change, so the rules have to travel with the data. A dataset declaration is that rule set: an offline peer, a second device, and a member on another continent all reject the same malformed write, without ever agreeing on a leader.

Declaring a dataset

curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/datasets \
  -H 'Content-Type: application/json' -d '{
  "name": "articles", "displayName": "Articles",
  "idRule": "user", "deleteBy": "author",
  "search": { "title": "title", "text": "body" },
  "fields": [
    { "key": "title",     "kind": "string", "required": true, "mutableBy": "author" },
    { "key": "body",      "kind": "string", "mutableBy": "author" },
    { "key": "author",    "stamp": "creator" },
    { "key": "createdAt", "stamp": "createTime" },
    { "key": "updatedAt", "stamp": "modifyTime" }
  ] }'
# → 201 {"datasetDefId": "…"}

any type dataset add $SPACE $TYPE --draft @articles.json
Field Meaning
name Collection name, pinned and space-unique. Conflicts with built-ins, handler datasets or other definitions → 409 dataset.name_conflict; prop / schema are reserved → 400 request.invalid_field.
idRule auto (default: ids derived from the change, explicit client ids rejected) or user (caller-supplied, matched against idPattern / idMaxLen, defaults [A-Za-z0-9._:-]+ / 128).
deleteBy anyone (default) or author — requires a stamp: creator field; deletes by anyone else are dropped at apply.
search {title, text, scope?} — which fields the search indexer extracts. text is one key or a non-empty array of keys joined in order. scope picks the index scope (default basic).
dynamic Keep undeclared keys permitted.
skipHistory Exclude the dataset from version history.

Per field:

Field Meaning
key, kind Field name and leaf kind. kind may be omitted on stamped fields (creator ⇒ string, times ⇒ datetime instant).
required Must be present on create. Declarable only at creation; incompatible with stamp.
mutableBy Absent = write-once (writable only in the creating change). author (needs a creator stamp) or any allow later edits. Each allowed edit bumps modifyTime when declared.
stamp creator / createTime / modifyTime — derived at apply, client writes rejected.

A malformed declaration — unknown enum labels, mutableBy: author without a creator stamp, duplicate stamp kinds — fails with 400 request.invalid_field or 400 dataset.decl_invalid.

What's pinned and what patches

Behavioral parts are pinned for the definition's life: name, dynamic, idRule/idPattern/idMaxLen, deleteBy, skipHistory, and every field's key/kind/shape/scope/required/mutableBy/stamp. To change one, remove the definition and add a new one.

Display parts patch through PATCH …/datasets/:defId with the same {set, unset} shape as a property patch, over description, displayName, search.title, search.text, search.scope:

curl -X PATCH http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/datasets/$DEF \
  -d '{"set": {"displayName": "Posts", "search.text": ["body", "notes"]}}'

any type dataset patch $SPACE $TYPE $DEF --set '{"displayName":"Posts"}'

A pinned path → 400 dataset.immutable; an unknown defId404 sdk.not_found. A search-mapping patch applies as records re-index; already-indexed docs keep their extracted text until their object is next written.

Evolution is additive

  • POST …/datasets/:defId/fields201 {fieldDefId} appends a field. An added field is never required — it would reject the dataset's own history on fresh devices.
  • DELETE …/datasets/:defId/fields/:fieldId drops a field definition. Stored values stay; later writes to the field are rejected as undeclared on non-dynamic datasets. A removal that would invalidate the rest of the declaration (the creator stamp of an author-gated dataset) is refused.
  • DELETE …/datasets/:defId tombstones the definition. Existing data is not cleaned up; subsequent writes drop once peers apply the removal; the search index evicts lazily.
any type dataset field add    $SPACE $TYPE $DEF --field '{"key":"notes","kind":"string","mutableBy":"any"}'
any type dataset field remove $SPACE $TYPE $DEF $FIELD
any type dataset remove       $SPACE $TYPE $DEF

Reading the definition back

GET …/types/:typeId/datasets returns the compiled view:

{ "datasets": [ { "id": "…", "name": "articles", "displayName": "Articles",
    "idRule": "user", "deleteBy": "author",
    "search": { "title": "title", "text": "body" },
    "fields": [ { "id": "…", "key": "title", "kind": "string", "scope": "synced",
                  "required": true, "mutableBy": "author" } ] } ] }

A definition whose folded declaration fails validation is listed with invalid: true and invalidReason — it never registers or accepts data, but stays visible so it can be repaired or removed. Definitions racing in from other members resolve deterministically: the smallest definition id wins; built-in names always win; the loser stays listed but never registers.

Runtime datasets also appear in the space's discovery document, GET /v1/spaces/:spaceId/datasets, as JSON Schema with typeId and the behavioral keywords x-mutable-by, x-stamp, x-delete-by, x-id / x-id-pattern / x-id-max-length, x-search, plus the standard required list. any datasets $SPACE prints the same.

Writing and reading records

The data path is the ordinary dataset surface — no new endpoints:

# write (the object must carry the owning type; attach it at create via "types")
curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/modify -d '{
  "objectId": "'$OBJ'", "dataset": "articles",
  "records": [ { "id": "a1", "ops": [
    { "type": "$set", "path": "", "value": { "title": "Hello", "body": "…" } } ] } ] }'

# read
curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/query \
  -d '{"objectId": "'$OBJ'", "dataset": "articles", "sort": ["-createdAt"]}'

See Writing data and Reading data for the full modify / query bodies, and Subscribe for live updates. idRule: user datasets additionally get idempotent batch ingest through Upsert.

Note. Records live on objects carrying the owning type, so the first write to a fresh object fails until the type is attached. Stamped fields (creator, createTime, modifyTime) are instants and identities the server fills in; a payload that sets them is rejected.