Source: https://docs.any.org/tutorial/properties.html # 2. Properties A type is what an object **is**: a named set of property definitions, plus the layout and the parts a client renders. Set it on an object and the object gains those columns — each with a kind the server checks, a descriptor that says how to render it, and a stable handle you resolve it by. This part builds a password manager — a `credential` type — and it never needs anything beyond this level. `API` and `SPACE` come from the [tutorial setup](https://docs.any.org/tutorial/index.md.txt#before-you-start); this part creates its own type and object. Use the dummy values shown. ## Create the type ```bash CRED=$(curl -s -X POST $API/spaces/$SPACE/types -H 'content-type: application/json' \ -d '{"name": "Credential", "xKey": "credential"}' | jq -r .typeId) ``` `xKey` is required: it is the programmatic handle the type resolves by, and it must survive renames — the display `name` can change freely. A handle already used by another type or collection in the space is `409 type.xkey_conflict` — the two share one namespace. ## Add properties Properties are added one at a time. Each has a `kind` — the storage contract, pinned for life — and an `xFormat` descriptor that says what the value means: ```bash add() { curl -s -X POST $API/spaces/$SPACE/types/$CRED/properties \ -H 'content-type: application/json' -d "$1" | jq -r .propId; } SITE=$(add '{"name": "Site", "xKey": "site", "kind": "string", "xFormat": {"type": "url"}}') LOGIN=$(add '{"name": "Login", "xKey": "login", "kind": "string", "xFormat": {"type": "text"}}') SECRET=$(add '{"name": "Secret", "xKey": "secret", "kind": "string", "xFormat": {"type": "text"}}') ROTATED=$(add '{"name": "Rotated", "xKey": "rotated", "kind": "datetime", "xFormat": {"type": "date"}}') CATEGORY=$(add '{"name": "Category", "xKey": "category", "kind": "array", "xFormat": {"type": "choice", "options": { "work": {"name": "Work", "color": "blue", "pos": "a0"}, "personal":{"name": "Personal","color": "green", "pos": "a1"}}}}') ``` The CLI spelling of the first one — an alternative, not a second step; `site` already exists once the HTTP call ran: ```bash any type property add $SPACE $CRED --name Site --xkey site --kind string --x-format '{"type":"url"}' ``` | Kind | Descriptor `type` | Value on the wire | | --- | --- | --- | | `string` | `text`, `url`, `email`, `phone` | a string | | `number` | `number`, `currency`, `percent` | a number | | `boolean` | `checkbox` | `true` / `false` | | `datetime` | `date`, `datetime` | `{"$date": "2026-09-08T00:00:00.000Z"}` — a `date` lands on midnight UTC | | `array` | `choice` | an array of option **keys**: `["work"]` even for a single choice | | `array` | `relation` | `["any://"]` — links to other objects | The kind is what the server enforces on every write; the descriptor is a hint for clients and can be changed later. The full vocabulary is in [Data types](https://docs.any.org/database/data-types.md.txt). ## The three ids You now hold three different identifiers, and they are never interchangeable: | id | what it is | where it comes from | | --- | --- | --- | | `typeId` (`$CRED`) | the content-addressed id of the type | `POST …/types`, `GET …/types` | | `xKey` (`site`) | the stable handle you declared | your own code | | `propId` (`$SITE`) | the content-addressed id of a property — **the write key** | `GET …/types/:typeId/properties` | Values are stored and written by `propId`, never by xKey: a write keyed by an xKey fails with `property.not_found`. A client resolves `xKey → propId` once from the property list and caches the map: ```bash curl -s $API/spaces/$SPACE/types/$CRED/properties # → {"properties": [{"id": "", "xKey": "site", "name": "Site", "kind": "string", "xFormat": {…}}, …]} ``` ## Store a credential Set the type at create and pass its values under its id. The `any` group still holds the name: ```bash GH=$(curl -s -X POST $API/spaces/$SPACE/objects -H 'content-type: application/json' -d '{ "type": "'$CRED'", "initialProperties": { "any": {"name": "GitHub"}, "'$CRED'": {"'$SITE'": "https://github.com", "'$LOGIN'": "ada", "'$SECRET'": "correct horse battery staple", "'$CATEGORY'": ["work"], "'$ROTATED'": {"$date": "2026-09-01T00:00:00Z"}}}}' | jq -r .objectId) ``` Notice where the values went: under the type's id, not at the top of the object. A type is a **namespace** for properties, and there are no global properties. Every property belongs to exactly one owner, is defined there, and its value lives on the object under that owner's id — even `name` and `description` sit in the universal `any` namespace rather than on the object itself. The property list of an owner is the whole vocabulary you can write under its id, and nothing outside that list is a property at all. A value that does not fit is refused: a number under `site` breaks the pinned kind (`400 property.kind_mismatch`); a bare string under `rotated` or an option not wrapped in an array breaks the descriptor (`400 property.format_violation`). The type is a contract, not a convention. Later writes go through the owner-scoped set: ```bash curl -s -X POST $API/spaces/$SPACE/properties/$GH/set/$CRED -H 'content-type: application/json' \ -d '{"patch": {"'$SECRET'": "new secret", "'$ROTATED'": {"$date": "2026-09-08T00:00:00Z"}}}' ``` > **Why it matters.** The secret is a CRDT value in a space that is end-to-end encrypted on your device; relay nodes carry ciphertext and hold no key ([Encryption](https://docs.any.org/understanding/encryption.md.txt)). A password manager at this level is one type and five properties — there is no vault format to design and no server to trust with plaintext. ## Ask questions of the columns Property paths on the wire are `.`. Filter and sort on them like any other field: ```bash # every work credential, oldest rotation first curl -s -X POST $API/spaces/$SPACE/objects/query -H 'content-type: application/json' -d '{ "filter": {"$and": [{"any.type": "'$CRED'"}, {"any.collections": {"$nin": ["bin"]}}, {"'$CRED.$CATEGORY'": "work"}]}, "sort": ["'$CRED.$ROTATED'"], "limit": 50}' ``` ```bash # rotated more than 90 days ago curl -s -X POST $API/spaces/$SPACE/objects/query -H 'content-type: application/json' -d '{ "filter": {"'$CRED.$ROTATED'": {"$lt": {"$date": "2026-06-10T00:00:00Z"}}}}' ``` Two things to notice. `{"any.type": ""}` is "objects of this type" — plain equality on a scalar, because an object has exactly one type. And the one exclusion. A type definition is itself an object row, but it carries the marker `__type__` in `any.type` rather than its own id, so it never turns up among its own objects and needs no exclusion clause. A binned object keeps its type, so an ordinary list leaves out `bin` members with `{"any.collections": {"$nin": ["bin"]}}` ([Reading data](https://docs.any.org/database/reading-data.md.txt)). The same body against `…/objects/query/subscribe` is a live list — the vault view of your password manager updates as entries change on any device. ## One object, one type and any number of collections The type says what the object **is**, and there is exactly one of it: it is the primary, and representation and behaviour come from it alone. Everything else an object belongs to is a **collection** — a category you file objects under. A collection says nothing about how its members render or behave — no layout, no parts. It carries only the columns that make sense for everything in that category, and filing an object under it adds those columns to the row under the collection's id. An object can be filed under any number of them, and none of them knows about the rest. Say the GitHub account is also something you pay for. "Paid subscription" is not what the object *is* — it is still a credential — so it is a collection, `subscription`, with a price and a renewal date: ```bash SUB=$(curl -s -X POST $API/spaces/$SPACE/collections -H 'content-type: application/json' \ -d '{"name": "Subscription", "xKey": "subscription"}' | jq -r .collectionId) PRICE=$(curl -s -X POST $API/spaces/$SPACE/collections/$SUB/properties -H 'content-type: application/json' \ -d '{"name": "Price", "xKey": "price", "kind": "number", "xFormat": {"type": "currency"}}' | jq -r .propId) RENEWS=$(curl -s -X POST $API/spaces/$SPACE/collections/$SUB/properties -H 'content-type: application/json' \ -d '{"name": "Renews", "xKey": "renews", "kind": "datetime", "xFormat": {"type": "date"}}' | jq -r .propId) curl -s -X POST $API/spaces/$SPACE/properties/$GH/collections/$SUB # file it under the collection curl -s -X POST $API/spaces/$SPACE/properties/$GH/set/$SUB -H 'content-type: application/json' \ -d '{"patch": {"'$PRICE'": 4, "'$RENEWS'": {"$date": "2026-10-01T00:00:00Z"}}}' ``` CLI: `any collection create $SPACE --name Subscription --xkey subscription` (the reply's id is `SUB`) and `any object collection attach $SPACE $GH $SUB` — alternatives to the two calls above, not a repeat. Creating a collection is the type create minus `layout`, and its four property routes are the type ones with a different owner segment — same bodies, same patch grammar, same error codes. Read the row back and it holds both groups, with `any.type` naming the one type and `any.collections` listing what it is filed under: ```json { "id": "bafyreig…", "any": { "name": "GitHub", "type": "", "collections": [""] }, "": { "": "https://github.com", "": "ada", "…": "…" }, "": { "": 4, "": {"$date": "2026-10-01T00:00:00.000Z"} } } ``` The object now answers to both questions: `{"any.type": "'$CRED'"}` lists it among the credentials, `{"any.collections": "'$SUB'"}` among the subscriptions, and a sort on `.` puts it in the renewal calendar. Passing `"collections": ["'$SUB'"]` at create does the same in one call; `DELETE …/properties/$GH/collections/$SUB` unfiles it again. Three rules make this simple rather than clever: - **One type, any number of collections.** The type is what the object is, and the only source of its layout and parts — replacing it with `POST …/properties/:objectId/type/:typeId` swaps the whole answer, and there is no unset. Filing and unfiling a collection is additive and idempotent, and neither is a delete: the old group's values stay on the row as read-tolerant orphan data, and setting the owner again brings them back. - **Column names never collide.** Each owner is its own namespace, so paths are `.` and a type and a collection can both have a `name` or a `date` property with the values kept apart. Filing an object is opening a second namespace on it, not merging columns into one flat row. - **Layout and parts come from the type alone; property groups come from every owner.** Collections have no layout and no parts, but each adds its columns. A client renders the credential layout and shows the subscription columns in the property panel next to the credential ones. Pick a type when the thing needs a layout, a body or any other part — Credential, Person, Task. Pick a collection when it is a category over objects that keep their own type — Subscription, Reading list, Q3 launch ([Collections](https://docs.any.org/database/collections.md.txt)). ## Change the definition, not the data A property definition is a synced record like the values. Rename it, reorder it, add an option — none of that touches stored values: ```bash any type property patch $SPACE $CRED $SECRET --set '{"name": "Password"}' any type property option set $SPACE $CRED $CATEGORY finance --name Finance --color amber --pos a2 ``` A choice value stores the option **key** (`work`), never its label, so renaming an option is one definition write and zero object writes. What you cannot change is `kind`: it is the one thing every peer has already relied on when validating writes (`400 property.immutable`). Details in [Property lifecycle](https://docs.any.org/database/property-lifecycle.md.txt). ## Where this level ends You have objects with typed, validated, filterable columns — a schema that syncs with its data — and collections that categorise objects, with columns of their own, without changing what the object is. It scales to hundreds of credentials, contacts or books: one object per thing, one row each in the `objects` storage collection. It does not scale to a mailbox. Ten thousand emails as ten thousand objects means ten thousand rows in the space-wide storage collection, each with its own change history, for things that belong together, arrive in bulk and are mostly read as one list. The next part keeps them as **records on a single object** — a dataset with its own enforced schema, ids you supply, and an import that is safe to re-run. Next: [3. Datasets](https://docs.any.org/tutorial/datasets.md.txt). Reference: [Types and properties](https://docs.any.org/database/types-and-properties.md.txt), [Collections](https://docs.any.org/database/collections.md.txt).