Database/Types and properties

Types and properties

A type is what an object is: the layout a client renders, the parts that hold its content, and a namespace of property definitions. Every object has exactly one, in any.type.

A collection is what an object is filed under: property definitions and nothing else. Both are owners of property definitions, so everything below about defining, patching and removing a property holds verbatim for a collection's columns — only the owner segment of the route differs. See Collections.

Create a type

curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/types \
  -H 'Content-Type: application/json' \
  -d '{"name": "Book", "xKey": "book", "description": "things I read"}'
# → 201 {"typeId": "bafy…"}
any type create $SPACE --name Book --xkey book

Body: {name?, description?, iconCid?, xKey, layout?, hidden?, meta?}. xKey is required — it is the stable programmatic handle a type resolves by; the display name is not a resolution key and may be renamed freely. Clients derive it as a slug of the name. An empty xKey is 400 type.xkey_required; one that collides with an existing type's or collection's xKey or id in the same space is 409 type.xkey_conflict (details: {xKey, existingTypeId}, or existingCollectionId when a collection holds the handle — the two surfaces share one namespace).

Property definitions are not part of the create body — a properties key, like any unknown key, is 400 request.unknown_field. Create the type, then add properties one by one.

Create a type when the thing needs a layout, parts or a body of its own; create a collection when it is a facet that only adds columns to objects keeping their own type (Collections).

Well-known types — person, organization, meeting, journal, deal — are not created here: the server's usecase catalog installs them (POST /v1/catalog/:usecaseId/setup) so every client and member lands on one definition (Bundles). A type you create under a handle the catalog later installs blocks that install with 409 type.xkey_conflict.

List and read types

curl http://127.0.0.1:7001/v1/spaces/$SPACE/types
any type list $SPACE

Each row is {id, name?, description?, iconCid?, xKey, builtIn?, layout?, hidden?, meta?}; flags are omitted when false, so read an absent builtIn or hidden as false. The list starts with four synthetic built-ins — any (the universal group: any.name, any.description, any.icon, any.tags, any.type, any.collections), spaceIndex, type (the meta-type: xkey, layout, hidden, meta) and collection (the shape of collection objects) — then every registered built-in type (the hidden page and dataview), then user types; hidden types appear only with includeHidden=true. Built-ins report builtIn: true with xKey equal to their id, which reserves those ids against user definitions. The four synthetic ids are never set on an object; a type picker skips them.

GET …/types/:typeId and GET …/types/:typeId/properties answer 404 type.not_found for an unknown id, and 400 type.not_a_type when the id names a user collection — read that one through …/collections/:collectionId. A built-in collection (miniapp, bin) has no type row and answers 404 type.not_found. A 200 [] from the properties list always means "exists, no properties yet".

Parts and layout

A type is more than its columns. Its parts are the display units a client renders for an object of the type — a body, a transcript, a task list — each owning storage collections a module serves (POST …/types/:typeId/parts; see Modules and Runtime datasets). Its layout is the descriptor a client renders the object with: {"type": "<slug>", "config": {…}}, your vocabulary, opaque to the server. Both are the type's alone — a collection has neither.

Because an object has exactly one type, its layout and its parts are that type's, with no primary-type contest to resolve. Set the layout at create or through PATCH …/types/:typeId ({name?, description?, iconCid?, layout?, hidden?, meta?} — absent keeps, "layout": null clears; 400 type.registered on a built-in).

A document type is the built-in page (hidden, one editor part, no properties) or a user type with an editor part, registered as a bundle so every device agrees on one. See Page.

Two more flags live on the type: hidden keeps it out of GET …/types (pass includeHidden=true to see it; GET …/types/:typeId always resolves it) — a bundle root is hidden when its install asks for it — and meta is an open bag of consumer flags, one string, bool or number per key, patched per key through PATCH …/types/:typeId (null unsets) so devices touching different keys merge. The server interprets none of the keys.

Add a property

curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/properties \
  -H 'Content-Type: application/json' \
  -d '{"name": "Author", "xKey": "author", "kind": "string"}'
# → 201 {"propId": "bafy…"}
any type property add $SPACE $TYPE --name Author --xkey author --kind string

The same body on POST …/collections/$COLL/properties adds a column to a collection.

Field Meaning
name, description Display metadata; mutable.
xKey The property's handle — an alias, unique within the owning type or collection (409 property.xkey_conflict), mutable. The server never keys values by it — values are stored and written by propId.
kind string / number / boolean / array / object / datetime. Required and pinned at first write.
xFormat The descriptor — slug, icon, order, options, relation targets, config; see Data types. Every path under it is mutable.
scope synced (default), account, or local. Pinned. derived is reserved for built-ins.
meta meta.index only: the search scope, or "none".

GET …/types/:typeId/properties returns {properties: [{id, name, description?, xKey, kind, scope, meta?, xFormat?}]} — this is where a client resolves xKey → propId before writing. Sort a property list by xFormat.pos, then id.

Why it matters. Definitions are synced records. A rename, a reorder via xFormat.pos or a new choice option written on one device converges on every member's device through the same CRDT as the data, with no schema-migration step.

Choice options

A choice property stores an array of option keys — one element unless xFormat.config.multiple is on. Options live under xFormat.options.<key> as {name, color, pos, meta?}, and the key is the value an object stores. Declare them at create or manage them through PATCH, one leaf at a time:

curl -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/properties \
  -H 'Content-Type: application/json' \
  -d '{"name": "Priority", "xKey": "priority", "kind": "array",
       "xFormat": {"type": "choice"}}'

curl -X PATCH http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/properties/$PROP \
  -H 'Content-Type: application/json' \
  -d '{"set": {"xFormat.options.high.name": "High",
              "xFormat.options.high.color": "red",
              "xFormat.options.high.pos": "a0"}}'
any type property option set $SPACE $TYPE $PROP high --name High --color red --pos a0

Membership is not enforced on value writes: deleting an option leaves existing values pointing at a dangling key. Because values store the key rather than the label, renaming an option is one definition write and zero object writes.

Patch a definition

PATCH …/types/:typeId/properties/:propId is a generic per-path {set, unset}; every op lands in one change and each leaf merges per path. PATCH …/collections/:collectionId/properties/:propId is the same call on a collection's column.

any type property patch $SPACE $TYPE $PROP --set '{"name": "Priority"}'
any type property patch $SPACE $TYPE $PROP --set '{"xFormat.config.multiple": true}'
any type property patch $SPACE $TYPE $PROP --unset xFormat.options.low
Paths Behaviour
name, description, xKey, meta.index, and every path under xFormat Mutable. A set targets a leaf — a string, number, boolean or array — never an object.
kind, scope, items, properties Pinned → 400 property.immutable.
containers (meta, xFormat, xFormat.options, xFormat.options.<key>, xFormat.relation, xFormat.config) Rejected on set; allowed on unset (unsetting an option key deletes the option).

Other failures: unknown path or wrong leaf type → 400 request.invalid_field; a slug that does not fit the kind, a reserved key or an unparseable xFormat.relation.filter → 400 property.format_invalid; a taken xKey → 409 property.xkey_conflict; a registered built-in → 400 type.registered; a user collection's id on a …/types route (or a user type's id on a …/collections one) → 400 type.not_a_type / 400 collection.not_a_collection, while a built-in in the wrong slot is 404 type.not_found / 404 collection.not_found; unknown ids → 404 sdk.not_found. Returns 204.

Remove a property

curl -X DELETE http://127.0.0.1:7001/v1/spaces/$SPACE/types/$TYPE/properties/$PROP   # → 204
any type property remove $SPACE $TYPE $PROP

Tombstones the definition. Existing values are not cleaned up; later writes to the removed id are dropped op by op.

Where values live

Values sit at record[ownerId][propId] on the object's row — {"<ownerId>": {"<propId>": value}} — where the owner is the object's type or one of its collections. On the wire and in filters the path is <ownerId>.<propId>. An object filed under two collections holds three groups: the type's and one per collection.

No property write creates its own owner: a value is admitted only while the object has that type or that collection. Set the type and file the collections first — at create, or through the routes in Objects. Keying a write by xKey fails with property.not_found; resolve it to the propId first. See Writing data.

  • Collections — the other owner of property definitions, and what filing an object does.
  • Data types — kinds, the descriptor vocabulary, instants, scopes.
  • Runtime datasets — declaring dataset schemas on a type.
  • Property lifecycle — pins, patches, removal and read tolerance in depth.
  • Data model — how a type, its collections and its datasets compose on one object.