# The standard library

The standard library is a set of ready-made collections for the things most
apps keep: messages, events, tasks, files, reactions. When your app keeps one
of those, use the standard one. Every other app that uses it reads your
records, and you read theirs.

```ts
import { reaction, comment, useSchemas } from '@minca/core/schemas';

await useSchemas(node, space.id, [reaction, comment]);
await node.records.put(
  space.id,
  reaction,
  { emoji: '👍' },
  {
    links: [{ rel: 'about', to: post.key }],
  },
);
```

`useSchemas` defines the ones the space doesn't know yet and leaves the rest
alone, so you can call it every time a space opens.

The definitions are ordinary collections. The protocol knows none of them.
They are drawn from atproto lexicons, Nostr NIPs, JSContact (RFC 9553),
JSCalendar (RFC 8984) and schema.org; issue
[#36](https://github.com/leifriksheim/minca/issues/36) has the sources.

## What a `std.*` name means

A collection name belongs to the space that defines it. There is no
registry, so two spaces could give one name different shapes. What lets two
apps read each other's records is a shared definition, and the library is
where apps find one.

`std.*` names belong to the library by convention. Peers don't refuse a
`std.*` definition of another shape, but the node's agent actions keep to it:

- `collections_standard` lists the library, and gives any definition in
  full.
- `collections_propose` takes a standard collection by name (`"std.event"`)
  and stores the library's definition for it.
- `collections_propose` refuses a `std.*` name the library doesn't have, and
  a `std.*` definition whose `schema`, `links`, `rules`, `topics` or
  `history` differ from the library's. The `title` and `description` may be
  your own.

Name your app's own collections for what they hold (`carpool.ride`), not
with a generic prefix (`app.ride`) another app may want too. To open
collections by what they hold rather than by name, see
[compatible definitions](https://minca.dev/collections/blueprints-and-guides.md#compatible-definitions).
See also [actions](https://minca.dev/automations/actions.md) and
[spec 02 §6.4](https://minca.dev/spec/02-records.md).

## How the library is shaped

People build several apps over the same records, often by asking an agent.
That only works if every app picks the same collection for the same thing.
Five rules keep the choice obvious.

1. **One collection per kind of thing.** Variants are optional fields. A
   photo, a video and a PDF are each a `std.file`, with `width`, `duration`
   or neither. Two collections an agent could pick for the same thing is a
   bug in the library.
2. **A group is a relation, not a field.** `std.tag` is a loose label, any
   number per record. `std.list` with a `std.list-item` per member is a
   named, ordered group that exists while empty: a folder, an album, a
   playlist, an itinerary (`kind`). So one file can be in a chat, a category
   and an album at once. A facet nearly every app filters by, like an
   event's `category`, may stay a field.
3. **What your app adds to a standard record is a record of its own.** Say
   your recipes app needs more on a `std.file`. Define `recipes.photo-note`
   with an `about` link to the file and `onePer: [link:about]`. Every app
   still reads the file. Never a parallel `recipes.photo`.
4. **Links share one vocabulary** (below). Where a record sits is always
   `in`: a channel, a board column or a list.
5. **A description says when not to use it.** Agents choose by reading
   descriptions, so a definition names the neighbour it could be mistaken
   for: "To vote on a std.proposal, use std.ballot."

## Conventions

- What several people edit is several records, not an array in one body. A
  list and its items, a document and its blocks, an order and its updates.
- "One per person" or "one per slot" is the `onePer` rule.
- A picture or a video in a `std.file` may carry a `thumbnail`: a small
  still, about 480 px on its longer side, that apps show in its place until
  someone opens it. It is small enough for every device to fetch ahead, so a
  long chat shows its pictures without fetching each file whole. A picture
  that is already small has none, and an app shows the picture itself. An
  app shows a thumbnail only for a `mime` of `image/` or `video/`, and only
  one of at most 256 KiB: it is the writer's word, like the rest of the body.
- Bodies have no `createdAt`, since the record has one. They carry only the
  times of the thing itself: `start`, `due`, `takenAt`.
- Link roles: `about` (what an annotation is on), `replyTo` and `root`
  (threads), `parent` (trees), `in` (member of a list, album, calendar,
  document or account), `shares` (quotes).
- What only one person should see is kept where only they read it: a block
  on the contact ([contacts](https://minca.dev/spaces/contacts.md)), and their own settings and
  reminders in the account registry, `node.account.space()`
  ([the node](https://minca.dev/node/node.md#records-and-the-account)).

See [spec 02 §4.3 and §7.3](https://minca.dev/spec/02-records.md).

## Fragments

Shared shapes like a time or an amount of money are built by the library's
`fragments`, so they are identical in every definition. You can use them in
your own definitions too.

| Fragment   | Shape                                                                                                                                                       |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `when`     | string 10–64, `x-kind: "when"`: RFC 3339 (`2026-03-01T18:30:00+01:00`), or `YYYY-MM-DD` for a whole day                                                     |
| `day`      | string of 10, `x-kind: "day"`: `YYYY-MM-DD`                                                                                                                 |
| `url`      | string 1–2048, `x-kind: "url"`                                                                                                                              |
| `timeZone` | string 1–64: an IANA zone, `Europe/Oslo`                                                                                                                    |
| DID        | string 1–256, `x-kind: "did"`: an account (built by `person`)                                                                                               |
| `money`    | `{ amount, currency }`, `x-kind: "money"`, both required. `amount` is a decimal string 1–32 (`"12.50"`), never a number. `currency` is ISO 4217             |
| `address`  | `{ street, locality, region, postcode, country }`, each optional. `country` is ISO 3166-1 alpha-2                                                           |
| `place`    | `{ name, address, lat, lon, cells }` (built by `placeRef`), each optional. WGS 84. `cells` is up to 8 geohash cells ([Places and areas](#places-and-areas)) |
| `blob`     | `{ hash, size, mime, name?, key?, salt? }`, `x-kind: "file"`: what `records.addFile` gives                                                                  |
| `image`    | `{ blob, alt? }`, `x-kind: "image"`, `alt` ≤ 2000                                                                                                           |
| `position` | string 1–200, `x-kind: "position"` ([Positions](#positions))                                                                                                |

A `blob`'s `hash` is the file's id (64 hex characters), and its `key` and
`salt` open it (22 base64url characters each). See [storage](https://minca.dev/node/storage.md#files).

`when` and `day` are checked only for length until the schema dialect has
`format` (Planned, below).

## The definitions

How to read the tables:

- A **bold** field is required.
- `string 1–200` gives its shortest and longest length.
- `[]` is a list, with its most items in brackets.
- `a`/`b` is a choice of values.
- Links read `role` → what it may point at, and how many.
- Rules are `onePer`, `fixed` and `final`, and whether it has a contract.

Who may add, change and remove records isn't up to the definition, but to
the space's grants. In the `community` template, members change only what
they made, moderators change anything, and only moderators and admins write
in the collections in `STAFF_COLLECTIONS` ([spaces](https://minca.dev/permissions/permissions.md#presets)).

The tables are made from the definitions in `@minca/core/schemas` each time
[minca.dev](https://minca.dev/collections/standard-library.md#the-definitions)
is built, so they always match the code.

### Annotations

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.reaction` | An emoji reaction to any record. | `respondingTo` DID; **`emoji`** string 1–16; topic `respondingTo` | `about` → `*`, one | `onePer: [@author, link:about, emoji]` |
| `std.comment` | A comment on any record, optionally replying to another comment. To chat, use std.message. | `respondingTo` DID; **`text`** string 1–10000; `mentions` DID[] (≤ 64); `replyingTo` DID; topics `mentions`, `replyingTo`, `respondingTo` | `about` → `*`, one; `replyTo` → `std.comment`, one; `shares` → `*`, one | — |
| `std.tag` | A loose label on one record, like #urgent: many per record, and anyone may add one. For a named group that exists while empty, use std.list; for a moderator’s label that apps act on, use std.label. | **`label`** string 1–100 | `about` → `*`, one | `onePer: [@author, link:about, label]` |
| `std.rating` | A score from 1 to 5, one per person per thing, changed by rating again. | **`score`** integer 1–5; `review` string ≤ 10000 | `about` → `*`, one | `onePer: [@author, link:about]` |
| `std.highlight` | A passage marked in a text, with an optional note. | **`quote`** string 1–10000; `prefix` string ≤ 500; `suffix` string ≤ 500; `note` string ≤ 10000 | `about` → `*`, one | — |
| `std.pin` | A record pinned for everyone by a moderator. To keep something for yourself, put it on a std.list. | `note` string ≤ 500 | `about` → `*`, one | `onePer: [link:about]` |
| `std.report` | A report that a record breaks the rules, for moderators. | **`reason`** `spam`/`abuse`/`sexual`/`misleading`/`illegal`/`other`; `note` string ≤ 2000 | `about` → `*`, one | `onePer: [@author, link:about]` |
| `std.label` | A moderator’s label that apps act on, like nsfw (blurred) or spoiler (hidden). Members label things with std.tag. | **`value`** string 1–64 | `about` → `*`, one | `onePer: [link:about, value]` |
| `std.claim` | Someone taking something on, one per thing, first come: a task, an item to bring, a std.slot to book. | `respondingTo` DID; `note` string ≤ 500; topic `respondingTo` | `about` → `*`, one | `onePer: [link:about]` |

### People

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.profile` | How someone presents themselves: one per person. | `name` string ≤ 100; `bio` string ≤ 2000; `avatar` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `banner` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `pronouns` string ≤ 50; `links` { `title` string ≤ 100; **`url`** url 1–2048 }[] (≤ 16); `bot` boolean | — | `onePer: [@author]` |
| `std.space-profile` | How the space presents itself: its name, a line about it and its picture. One per space: "space" is its id. | **`space`** string 1–256; `name` string ≤ 100; `bio` string ≤ 2000; `avatar` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `banner` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 } | — | `onePer: [space]` |
| `std.card` | An address-book entry for anyone, on Minca or not. Your own public self is std.profile; a Minca account you can message is std.contact. | `name` { `full` string ≤ 200; `given` string ≤ 100; `family` string ≤ 100 }; `emails` { **`address`** string 1–320; `label` string ≤ 50 }[] (≤ 16); `phones` { **`number`** string 1–64; `label` string ≤ 50 }[] (≤ 16); `addresses` { **`address`** { `street` string ≤ 500; `locality` string ≤ 200; `region` string ≤ 200; `postcode` string ≤ 32; `country` string 2–2 }; `label` string ≤ 50 }[] (≤ 16); `organization` string ≤ 200; `jobTitle` string ≤ 200; `birthday` day 10–10; `photo` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `urls` url 1–2048[] (≤ 16); `note` string ≤ 10000 | — | — |
| `std.follow` | Following a person: one per person followed. | **`did`** DID; `space` string ≤ 256; topic `did` | — | `onePer: [@author, did]` |
| `std.block` | Someone to hide and keep away. Keep it in your own space. | **`did`** DID; `until` when 10–64 | — | `onePer: [@author, did]` |
| `std.mute` | Someone to quiet for a while. Keep it in your own space. | **`did`** DID; `until` when 10–64 | — | `onePer: [@author, did]` |
| `std.status` | What someone is up to now: one per person. | `text` string ≤ 280; `emoji` string ≤ 16; `until` when 10–64 | — | `onePer: [@author]` |

### Messaging and publishing

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.message` | A chat message, in order with the rest, optionally in a std.channel, replying to another or sharing a record. Posts to a feed are std.post; remarks on a record are std.comment. | **`text`** string 1–10000; `mentions` DID[] (≤ 64); `replyingTo` DID; topics `link:in`, `mentions`, `replyingTo` | `in` → `std.channel`, one; `replyTo` → `std.message`, one; `shares` → `*`, one | — |
| `std.channel` | A named conversation within a space. | **`name`** string 1–100; `topic` string ≤ 500; `position` position 1–200 | — | — |
| `std.direct` | A message only the people it is sent to can read. | **`to`** DID[] (≤ 16); **`data`** string ≤ 60000; **`boxes`** { **`to`** DID; **`sealed`** string ≤ 1000 }[] (≤ 17); topic `to` | — | — |
| `std.post` | A post to a feed, optionally replying to or quoting another. Chat in order is std.message. | `text` string ≤ 10000; `images` { **`blob`** { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `alt` string ≤ 2000 }[] (≤ 8); `langs` string 2–35[] (≤ 3); `mentions` DID[] (≤ 64); `replyingTo` DID; topics `mentions`, `replyingTo` | `replyTo` → `std.post`, one; `root` → `std.post`, one; `shares` → `*`, one | — |
| `std.repost` | Passing a record on as it is: one per person per record. | `respondingTo` DID; topic `respondingTo` | `about` → `*`, one | `onePer: [@author, link:about]` |
| `std.article` | Long-form writing, in CommonMark. | `mentions` DID[] (≤ 64); **`title`** string 1–300; `summary` string ≤ 1000; `content` string ≤ 200000; `cover` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `slug` string 1–200; `publishedAt` when 10–64; `draft` boolean; topic `mentions` | `in` → `std.publication`, one | — |
| `std.publication` | A blog or newsletter that articles are published in. | **`title`** string 1–200; `description` string ≤ 2000; `icon` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 } | — | — |
| `std.doc` | A document people write together, as std.doc-blocks in it. An uploaded file, like a PDF or .docx, is a std.file. | **`title`** string 1–500 | — | — |
| `std.doc-block` | One block of a document: a paragraph, heading, list item, image… | `mentions` DID[] (≤ 64); **`type`** `paragraph`/`heading1`/`heading2`/`heading3`/`bullet`/`numbered`/`todo`/`quote`/`code`/`image`/`divider`; `text` string ≤ 20000; `checked` boolean; `language` string ≤ 32; `image` { **`blob`** { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `alt` string ≤ 2000 }; `position` position 1–200; topic `mentions` | `in` → `std.doc`, one; `parent` → `std.doc-block`, one | — |
| `std.wiki-page` | A page anyone can edit, one per name, keeping every version. For a note of one’s own, use std.note. | **`slug`** string 1–200; **`title`** string 1–300; `content` string ≤ 200000 | — | `onePer: [slug]` |
| `std.note` | A note: a title and some text. A page anyone edits under one name is std.wiki-page; a document several people write at once is std.doc. | `mentions` DID[] (≤ 64); `title` string ≤ 500; `content` string ≤ 200000; `pinned` boolean; `color` string ≤ 32; topic `mentions` | — | — |
| `std.call` | A missed call, or one that ended and who was in it. | **`status`** `missed`/`ended`; `to` string ≤ 256; **`startedAt`** string ≤ 64; `endedAt` string ≤ 64; `people` string ≤ 256[] (≤ 64); topics `to`, `people` | — | — |

### Lists

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.list` | A named, ordered group of anything that exists while empty: a checklist, a folder, an album, a playlist, a category (`kind`). Each member is a std.list-item in it, about the record it stands for. For loose labels, use std.tag. | **`title`** string 1–200; `description` string ≤ 2000; `icon` string ≤ 16; `kind` string ≤ 32 | — | — |
| `std.list-item` | One thing on a list: text, a link, a person or a record. | `text` string ≤ 1000; `url` url 1–2048; `did` DID; `checked` boolean; `quantity` number ≥ 0; `position` position 1–200; topic `did` | `in` → `std.list`, one; `about` → `*`, one | — |

### Files and media

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.file` | Any file people share: a document, photo, video or recording, on its own or attached to a record (about). The only collection for files: a photo or video is a std.file too. For a folder, album or category of files, use std.list with a std.list-item about each file. | **`name`** string 1–255; **`mime`** string 1–255; `size` integer ≥ 0; `blob` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `url` url 1–2048; `alt` string ≤ 2000; `width` integer ≥ 1; `height` integer ≥ 1; `duration` number ≥ 0; `takenAt` when 10–64; `place` { `name` string ≤ 200; `address` { `street` string ≤ 500; `locality` string ≤ 200; `region` string ≤ 200; `postcode` string ≤ 32; `country` string 2–2 }; `lat` number -90–90; `lon` number -180–180; `cells` string 1–12[] (≤ 8) }; `thumbnail` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `captions` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 } | `about` → `*`, one | — |
| `std.track` | A piece of music, by title, artists and ids. Its audio, when the space holds it, is a std.file it links to. A playlist is a std.list. | **`title`** string 1–300; `artists` string 1–200[] (≤ 20); `album` string ≤ 300; `duration` number ≥ 0; `isrc` string ≤ 12; `mbid` string ≤ 36 | `file` → `std.file`, one | — |
| `std.play` | A track someone listened to, and when: about the std.track when the space has it. | **`title`** string 1–300; `artists` string 1–200[] (≤ 20); `album` string ≤ 300; `duration` number ≥ 0; `isrc` string ≤ 12; `mbid` string ≤ 36; **`playedAt`** when 10–64 | `about` → `std.track`, one | — |

### Time and planning

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.calendar` | A calendar of events. | **`name`** string 1–200; `color` string ≤ 32 | — | — |
| `std.event` | Something happening at a time and place. | **`title`** string 1–500; `description` string ≤ 10000; `category` string ≤ 100; **`start`** when 10–64; `end` when 10–64; `tz` string 1–64; `allDay` boolean; `place` { `name` string ≤ 200; `address` { `street` string ≤ 500; `locality` string ≤ 200; `region` string ≤ 200; `postcode` string ≤ 32; `country` string 2–2 }; `lat` number -90–90; `lon` number -180–180; `cells` string 1–12[] (≤ 8) }; `url` url 1–2048; `rrule` string 1–1000; `status` `confirmed`/`tentative`/`cancelled`; topic `place.cells` | `in` → `std.calendar`, one | — |
| `std.rsvp` | Whether someone is coming to an event: one answer per person. | `respondingTo` DID; **`status`** `going`/`maybe`/`no`; `guests` integer 0–100; `note` string ≤ 500; topic `respondingTo` | `about` → `std.event`, one | `onePer: [@author, link:about]` |
| `std.slot` | A time one person can book, by a std.claim about it. | **`start`** when 10–64; **`end`** when 10–64; `tz` string 1–64; `note` string ≤ 1000 | `in` → `std.calendar`, one | — |
| `std.column` | A column on a task board (To do, Doing, Done), in a std.project or on its own; tasks sit in it. | **`name`** string 1–200; `position` string 1–200 | `in` → `std.project`, one | — |
| `std.task` | A thing to do: a to-do, an issue, a card on a board. `status` for plain lists; on a board it sits in a std.column. Something to be reminded of about an existing record is a std.reminder. | **`title`** string 1–500; `notes` string ≤ 10000; `position` string 1–200; `due` when 10–64; `status` `todo`/`doing`/`done`/`cancelled`; `assignees` DID[] (≤ 20); `priority` integer 0–4; topic `assignees` | `in` → `std.column`, `std.project`, one; `parent` → `std.task`, one | — |
| `std.project` | A project that tasks and board columns are in. | **`name`** string 1–200; `description` string ≤ 10000; `status` `planned`/`active`/`paused`/`done`/`cancelled`; `due` when 10–64 | — | — |
| `std.time-entry` | Time someone spent on something. | **`start`** when 10–64; `end` when 10–64; `note` string ≤ 1000 | `about` → `*`, one | — |
| `std.reminder` | A nudge at a time about an existing record (about), for its author. A thing to do is a std.task with `due`. | **`at`** when 10–64; `note` string ≤ 1000; `done` boolean | `about` → `*`, one | — |
| `std.habit` | Something to do regularly. | **`name`** string 1–200; `schedule` string ≤ 1000; `target` number ≥ 0; `unit` string ≤ 32; `position` position 1–200 | — | — |
| `std.checkin` | A habit done on a day: one per person per day. | **`date`** day 10–10; `value` number; `note` string ≤ 1000 | `about` → `std.habit`, one | `onePer: [@author, link:about, date]` |

### Places and travel

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.place` | A place: a name, an address, a point on the map. | **`name`** string 1–200; `address` { `street` string ≤ 500; `locality` string ≤ 200; `region` string ≤ 200; `postcode` string ≤ 32; `country` string 2–2 }; `lat` number -90–90; `lon` number -180–180; `cells` string 1–12[] (≤ 8); `category` string ≤ 100; `url` url 1–2048; topic `cells` | — | — |
| `std.visit` | Someone having been at a place. | **`at`** when 10–64; `note` string ≤ 2000 | `about` → `std.place`, one | — |
| `std.trip` | A trip, with dates. Its plan is a std.list of kind "itinerary", with items about its events and places. | **`title`** string 1–200; `start` day 10–10; `end` day 10–10; `note` string ≤ 10000 | — | — |
| `std.location` | Where someone is now: one per person. | **`lat`** number -90–90; **`lon`** number -180–180; `accuracy` number ≥ 0; **`at`** when 10–64 | — | `onePer: [@author]` |

### Home and life

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.recipe` | A recipe: ingredients and steps. | **`title`** string 1–300; `description` string ≤ 2000; `ingredients` { **`text`** string 1–500; `quantity` number ≥ 0; `unit` string ≤ 32 }[] (≤ 200); `steps` string 1–5000[] (≤ 200); `servings` integer ≥ 1; `prepMinutes` integer ≥ 0; `cookMinutes` integer ≥ 0; `image` { **`blob`** { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `alt` string ≤ 2000 }; `source` url 1–2048 | — | — |
| `std.meal` | A planned meal: one per day per meal. | **`date`** day 10–10; **`meal`** `breakfast`/`lunch`/`dinner`/`snack`; `note` string ≤ 1000 | `about` → `std.recipe`, one | `onePer: [date, meal]` |
| `std.journal-entry` | What someone wrote about a day: one entry per day. | **`date`** day 10–10; `content` string ≤ 200000; `mood` string ≤ 32 | — | `onePer: [@author, date]` |
| `std.measurement` | Something measured at a moment: a kind, a value, a unit. | **`kind`** string 1–64; **`value`** number; **`unit`** string 1–32; **`at`** when 10–64; `note` string ≤ 1000 | — | — |
| `std.workout` | A run, a ride, a session: when, how long, how far. | **`type`** string 1–64; **`start`** when 10–64; `duration` number ≥ 0; `distance` number ≥ 0; `route` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `note` string ≤ 2000 | — | — |
| `std.work` | A book, film, show, album or game. | **`kind`** `book`/`film`/`show`/`album`/`game`/`podcast`/`other`; **`title`** string 1–500; `creators` string 1–200[] (≤ 20); `year` integer 0–9999; `ids` { `isbn` string ≤ 17; `imdb` string ≤ 16; `mbid` string ≤ 36; `wikidata` string ≤ 16 }; `cover` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 } | — | — |
| `std.progress` | Where someone is with a book, film or game: one per person. | **`status`** `want`/`doing`/`done`/`dropped`; `percent` number 0–100; `finishedAt` when 10–64; `note` string ≤ 2000 | `about` → `std.work`, one | `onePer: [@author, link:about]` |

### Money and trade

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.expense` | Something one person paid for a group, to split and settle up. Your own bank records are std.transaction. | `owedBy` DID[] (≤ 63); **`title`** string 1–200; **`amount`** { **`amount`** string 1–32; **`currency`** string 3–3 }; **`paidBy`** DID; `split` { **`did`** DID; `share` string 1–32 }[] (≤ 100); `date` day 10–10; `note` string ≤ 2000; topics `paidBy`, `owedBy` | — | — |
| `std.settlement` | Money paid back from one person to another. | **`from`** DID; **`to`** DID; **`amount`** { **`amount`** string 1–32; **`currency`** string 3–3 }; `date` day 10–10; `note` string ≤ 1000; topics `from`, `to` | — | `fixed: [from, to, amount]` |
| `std.money-account` | A bank account, card or wallet that std.transactions are in. Not a person’s account. | **`name`** string 1–200; **`currency`** string 3–3; `kind` string ≤ 32 | — | — |
| `std.transaction` | Money in or out of a std.money-account. To split a cost with others, use std.expense. | **`amount`** { **`amount`** string 1–32; **`currency`** string 3–3 }; **`date`** day 10–10; `payee` string ≤ 200; `category` string ≤ 100; `note` string ≤ 1000; `cleared` boolean | `in` → `std.money-account`, one | — |
| `std.listing` | Something for sale, to give away or to lend. | **`title`** string 1–300; `description` string ≤ 10000; `price` { **`amount`** string 1–32; **`currency`** string 3–3 }; `category` string ≤ 100; `condition` `new`/`like-new`/`good`/`fair`/`for-parts`; `images` { **`blob`** { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 }; `alt` string ≤ 2000 }[] (≤ 12); `status` `available`/`reserved`/`sold`; `place` { `name` string ≤ 200; `address` { `street` string ≤ 500; `locality` string ≤ 200; `region` string ≤ 200; `postcode` string ≤ 32; `country` string 2–2 }; `lat` number -90–90; `lon` number -180–180; `cells` string 1–12[] (≤ 8) }; topic `place.cells` | — | — |
| `std.order` | An order for things listed: what, how many, for how much. | `respondingTo` DID; **`items`** { **`title`** string 1–300; **`quantity`** integer ≥ 1; `price` { **`amount`** string 1–32; **`currency`** string 3–3 } }[] (≤ 100); `total` { **`amount`** string 1–32; **`currency`** string 3–3 }; `note` string ≤ 2000; topic `respondingTo` | `about` → `std.listing`, many | — |
| `std.order-update` | A step in an order: accepted, paid, shipped, delivered, cancelled. | `respondingTo` DID; **`status`** `accepted`/`paid`/`shipped`/`delivered`/`cancelled`/`refunded`; `note` string ≤ 2000; topic `respondingTo` | `about` → `std.order`, one | — |

### Community and governance

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.poll` | A question with options to vote on. | **`question`** string 1–500; **`options`** string 1–200[]; `closed` boolean | — | `fixed: [options]` |
| `std.vote` | One person’s vote on a std.poll, changed by voting again. To vote on a std.proposal, use std.ballot: it is final and counts toward a decision. | `respondingTo` DID; **`choice`** integer ≥ 0; `choices` integer ≥ 0[] (≤ 100); topic `respondingTo` | `about` → `std.poll`, one | `onePer: [@author, link:about]` |
| `std.proposal` | Something put to the group to decide: people cast a std.ballot on its options, and a std.decision proves the outcome. For a quick question, use std.poll. | **`title`** string 1–300; `body` string ≤ 20000; **`options`** string 1–200[] (≤ 20); `closesAt` when 10–64; `status` `open`/`passed`/`rejected`/`withdrawn`; `quorum` integer 1–10000 | — | `fixed: [options]` |
| `std.ballot` | One person’s final vote on a std.proposal: it can’t be changed, so a std.decision can count it. For a poll, use std.vote. | `respondingTo` DID; **`choice`** integer ≥ 0; topic `respondingTo` | `about` → `std.proposal`, one | `onePer: [@author, link:about]`; `fixed: [choice]` |
| `std.decision` | What a proposal came to, proven by the ballots it cites. | **`outcome`** integer ≥ 0; **`proposal`** string 1–128; **`ballots`** string 1–128[] (≤ 256) | `about` → `std.proposal`, one | `onePer: [link:about]`; a contract |
| `std.goal` | A target the group reaches together, pledge by pledge. | **`title`** string 1–300; `body` string ≤ 20000; **`target`** integer 1–1000000000; **`unit`** string 1–50; `closesAt` when 10–64 | — | `fixed: [target, unit]` |
| `std.pledge` | A promise toward a goal: once per person, not changed once made. | `respondingTo` DID; **`amount`** integer 1–1000000000; `note` string ≤ 1000; topic `respondingTo` | `about` → `std.goal`, one | `onePer: [@author, link:about]`; `fixed: [amount]` |
| `std.goal-reached` | A goal reached, proven by the pledges it cites. | **`goal`** string 1–128; **`pledges`** string 1–128[] (≤ 256) | `about` → `std.goal`, one | `onePer: [link:about]`; a contract |
| `std.announcement` | News for everyone in the space. | **`title`** string 1–300; `text` string ≤ 20000 | — | — |
| `std.badge` | A badge that people can be awarded. | **`name`** string 1–100; `description` string ≤ 1000; `image` { **`hash`** string 64–64; **`size`** integer ≥ 0; **`mime`** string 1–255; `name` string ≤ 255; `key` string 22–22; `salt` string 22–22 } | — | — |
| `std.award` | A badge given to someone, once. | **`did`** DID; `note` string ≤ 1000; topic `did` | `about` → `std.badge`, one | `onePer: [link:about, did]` |

### Settings

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.setting` | A person’s setting for an app: one per app and key. | **`app`** string 1–200; **`key`** string 1–200; `value` any | — | `onePer: [@author, app, key]` |
| `std.host` | A host the space pays to keep it online, and anyone in it may chip in for. | **`url`** string 1–2048; `did` string 1–256; `name` string ≤ 100 | — | `onePer: [url]` |

### Rules

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.automation` | When records in the space are a certain way, or at set times, do something. | **`name`** string 1–120; `when` object; `every` string 9–100; **`then`** object; `by` DID; `picked` object; `paused` boolean; **`since`** when 10–64; `spaces` any; `open` string 1–2000; `app` { **`origin`** string 1–200; `name` string 1–80 } | — | — |
| `std.automation-run` | An automation acted on a record: once each, whichever device did it. | **`did`** string 1–500; **`ok`** boolean; **`at`** when 10–64; `made` string 1–256 | `automation` → `std.automation`, one; `about` → `*`, one | `onePer: [link:automation, link:about]` |
| `std.activity` | Someone at work on a record: working, waiting, done or failed. | **`state`** `working`/`waiting`/`done`/`failed`; `label` string 1–120; **`at`** when 10–64 | `about` → `*`, one | `onePer: [@author, link:about]` |

### Bots

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.bot` | How a community wants its bot to be, in the words of someone who may configure it. | **`bot`** DID; **`instructions`** string ≤ 2000 | — | `onePer: [@author, bot]` |
| `std.connect` | Someone asking to connect a bot to a service, or to disconnect it. | **`bot`** DID; **`kind`** string 1–40; `verifier` string 43–64; **`expires`** when 10–64; `disconnect` boolean | — | — |
| `std.connector` | A service a bot is connected to, said by the bot. | **`bot`** DID; **`kind`** string 1–40; **`account`** string 1–100; `reaches` string 1–200[] (≤ 100); **`by`** DID; **`since`** when 10–64 | — | `onePer: [@author, kind]` |

### Collections

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.blueprint` | Collections someone proposes for this space, not yet added. | **`title`** string 1–100; `description` string ≤ 1000; **`collections`** object[] (≤ 10); `from` string ≤ 300; `updates` string 1–100 | — | — |
| `std.guide` | How some of the space’s collections are meant to be used, for whoever writes to them next. | **`name`** string 1–100; **`title`** string 1–200; **`text`** string 1–20000; `collections` string 1–200[] (≤ 16) | — | `onePer: [name]` |

### Contacts and inboxes

| Name | What it is | Body | Links | Rules |
| --- | --- | --- | --- | --- |
| `std.contact` | Someone you can reach, and the space you share with them. | **`did`** string ≤ 256; **`name`** string ≤ 200; `space` string ≤ 256; `note` string ≤ 2000; `blocked` boolean; `username` string ≤ 320 | — | `onePer: [did]` |
| `std.contact-request` | A knock left for someone in a space you share, sealed so only they can open it. | **`to`** string ≤ 64; **`sealed`** string ≤ 16000 | — | — |
| `std.inbox` | Where people you share no space with can ask to become your contact. | **`gen`** integer ≥ 0; **`places`** string ≤ 200[] (≤ 3); `name` string ≤ 64; `username` string ≤ 320; `usernameUntil` number | — | — |
| `std.knock` | A knock left on an inbox, waiting for them to join. | **`space`** string ≤ 256; **`name`** string ≤ 64; **`invite`** string ≤ 12000; `username` string ≤ 320 | — | `onePer: [space]` |
| `std.space-invite` | An invite to a space, left for a contact in your space for two. | **`space`** string ≤ 256; **`invite`** string ≤ 12000 | — | `onePer: [space]` |

## About some of them

### `std.call`

Calls themselves are kept nowhere ([calls](https://minca.dev/calls/calls.md)). If the
space defines `std.call`, a call leaves a record of its history there.

| Field       | Type                    | Meaning                                      |
| ----------- | ----------------------- | -------------------------------------------- |
| `status`    | `"missed"` \| `"ended"` | Required.                                    |
| `to`        | string ≤ 256            | For a missed call: who was rung.             |
| `startedAt` | ISO date, ≤ 64          | Required.                                    |
| `endedAt`   | ISO date, ≤ 64          | For an ended call.                           |
| `people`    | string ≤ 256[] (≤ 64)   | For an ended call: every account seen in it. |

```json
{
  "status": "ended",
  "startedAt": "2026-09-26T10:00:00.000Z",
  "endedAt": "2026-09-26T10:14:02.311Z",
  "people": ["did:key:zDnaeSm3…", "did:key:zDnaeXL64…"]
}
```

The call a device is in is kept in `sessionStorage` under `minca-call` as
`{ space, call }`, so after a reload the page can offer to rejoin.

### Tasks

A task is `in` its column, or `in` its project when it has no column. A
column is `in` its project.

### Proven outcomes

Nobody declares a `std.decision` or a `std.goal-reached`. Each one cites the
records that prove it, by version id, and every device checks the proof
before it counts. Anyone in the space may write one, once, when the proof is
there.

- A `std.decision` cites the proposal's first version (`proposal`) and the
  ballots (`ballots`). It holds when the ballots about that proposal that
  chose `outcome` come from at least `quorum` different people. A proposal
  without a `quorum` can't be decided this way.
- A `std.goal-reached` cites the goal's first version (`goal`) and the
  pledges (`pledges`). It holds when the pledges add up to `target`,
  counting each person once.

```json
{ "outcome": 0, "proposal": "bay24o4l…", "ballots": ["bq7x…", "b3kd…", "bm2a…"] }
```

A ballot's `choice` and a pledge's `amount` are `fixed`, so what was counted
can't change. That's why `std.ballot` exists beside `std.vote`: a vote can be
changed, a ballot can't. See [contracts](https://minca.dev/contracts/contracts.md).

### `std.host`

A `std.host` names a host the space pays to keep it online. Anyone in the
space may chip in, and members' devices hand the host the space's pass. That
means naming a host sends it every member's pass, so the `community`
template leaves `std.host` to admins and moderators. See
[node](https://minca.dev/node/node.md#a-space-paying-for-itself) and
[spec 08 §2](https://minca.dev/spec/08-hosting.md).

### Automations, bots, blueprints and guides

- `std.automation` and `std.automation-run`: what a space does by itself
  when records change or at set times, and what each run did. See
  [automations](https://minca.dev/automations/automations.md).
- `std.bot`, `std.connect` and `std.connector`: how a community wants its
  bot to be, asking a bot's host to connect it to a service like GitHub, and
  what the bot is connected to. See [bots](https://minca.dev/automations/bots.md).

`std.blueprint` holds collections someone proposes for the space, not yet
added. `std.guide` says how some of the space's collections are meant to be
used. See [blueprints and guides](https://minca.dev/collections/blueprints-and-guides.md).

## Activity

`std.activity` shows that someone is at work on a record, for as long as it
lasts: "My bot is working on it…" under a message, or a spinner on a task.
Anyone may write one, but only about their own work.

| Name           | Body                                                                                                   | Links              | Rules                           |
| -------------- | ------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------- |
| `std.activity` | **`state`** `working`/`waiting`/`done`/`failed`; `label` string ≤ 120; **`at`** when, since this state | `about` → any, one | `onePer: [@author, link:about]` |

There's one per account per record, so writing again moves the same record
from `waiting` to `working` to `done` or `failed`. A label left out keeps
the one it had.

```ts
import { setActivity, activeNow } from '@minca/core/schemas';

await setActivity(node, space.id, message.key, 'working', 'Reading the thread');
```

A `working` or `waiting` activity whose `at` is more than five minutes old
(`ACTIVITY_STALE_SECONDS`) must be shown as over, since its writer may have
gone away. `activeNow(body)` tells you whether one is still going.

An agent or bot says what it's doing with the `activity_set` action
([actions](https://minca.dev/automations/actions.md)). A bot's automation runner marks what set each
automation off: `waiting` when queued, `working` with the automation's name
when it starts, then `done` or `failed`
([automations](https://minca.dev/automations/automations.md#running)).

## Bots

A bot is an account of its own that people invite to their spaces as a
member. The protocol can't tell bots and people apart. A bot says it is one
with `bot: true` on its own `std.profile`, which only it can write, and apps
may show that. It's the account's own word, like its name, so leaving it out
proves nothing.

A bot that may add collections adds `std.profile` where a space has none.
Besides its own automations, it runs the space's automations that name it in
`by`, made by members granted `/std.automation/instruct`. In the `community`
template, admins and moderators hold that grant. Nothing the bot writes sets
an automation off. See [automations](https://minca.dev/automations/automations.md#who-runs-an-automation).

What a bot writes shows as the bot, and every member's device checks it
against the bot's role. So a role that may post but not delete keeps a
misled bot from deleting.

## The space's profile

`std.space-profile` is how a space presents itself, the way `std.profile` is
how a person does. Apps show its `name` and `avatar` in place of the name
the space was founded with.

The founding name stays in the constitution, where nobody can change it. An
invite shows that name to someone who can't read the space yet, so the name
they see after joining may differ.

There's one per space. `space` holds the space's id, and `onePer: [space]`
gives everyone the same key, so changing it is editing one shared record.
Which edit stands follows the order of versions, never a writer's clock. A
reader takes only the record whose `space` is its own space's id.

Anyone who may write there could also start a rival record at that key, so
who may write it matters. It's in `STAFF_COLLECTIONS`: in a `community`,
admins and moderators can and members can't. In a `coop`, stewards can.

A space made before `std.space-profile` existed keeps the grants it was made
with. There, any member decides the name and picture, and outranks staff,
until an admin redefines the member role.

The example app reads no `std.space-profile` in a space for two. It names
the space for the other person.

## Text is Markdown

The `text` of a `std.message`, `std.comment` or `std.post` is Markdown:
CommonMark, with GitHub's tables and `~~strikethrough~~`. A plain sentence is
Markdown too, so an app that shows text as it is still shows it right.

````json
{ "text": "Shipped **today**:\n\n- the chat\n- `code` blocks\n\n```\nnpm run dev\n```" }
````

Draw it as elements, never as HTML, so a message can't put markup on the
screen. A screen gets this from the kit: `MentionText` draws it and
`MentionInput` writes it.

## Mentions and replies

A `std.message`, `std.comment` or `std.post` names the accounts it calls on
in `mentions`. When it replies to someone, it names their account in
`replyingTo`. `std.note`, `std.doc-block` and `std.article` have `mentions`
too.

```json
{ "text": "@Sam are you coming?", "mentions": ["did:key:zDnae…Sam"], "replyingTo": "did:key:zDnae…Sam" }
```

Fill them from what the person picked, like an `@name` they chose or the
message they replied to. Never fill them by matching text.

They are topics, as is a message's `in` link. So an automation of the
account's own can ask for only what mentions it, replies to it, or is in one
channel, and a carrier's wake can match these unread:

```json
{ "where": { "mentions": { "$contains": "$me" } } }
```

A `std.task`'s `assignees` is a topic the same way, so "assigned to me" is
`where: { "assignees": { "$contains": "$me" } }`.

A comment can be `about` any record. So commenting is how to mention someone
on a record whose definition has no `mentions`. Your own collections can
name a `mentions` field as a topic too. See
[automations](https://minca.dev/automations/automations.md#the-accounts-own) and
[spec 02 §8](https://minca.dev/spec/02-records.md).

## Responses

A record written in answer to someone's record names them in
`respondingTo`. It's the author of what it answers:

| Record                                      | `respondingTo`                      |
| ------------------------------------------- | ----------------------------------- |
| `std.reaction`, `std.comment`, `std.repost` | the author of what it is about      |
| `std.rsvp`                                  | the event's author                  |
| `std.vote`, `std.ballot`                    | the poll's or the proposal's author |
| `std.pledge`                                | the goal's author                   |
| `std.claim`                                 | the claimed record's author         |
| `std.order`, `std.order-update`             | the listing's or the order's author |

It's a topic, so "someone answered what I wrote" is
`where: { "respondingTo": { "$contains": "$me" } }`, and a carrier's wake can
match it unread. The link says the same to a node that can read, but a
carrier can't read a private space's links, so the account is written out.
Fill it from the author of the linked record, and leave it out when that's
the writer.

A comment can carry all three: `respondingTo`, `replyingTo` and `mentions`.

Some annotations name nobody. A `std.report` doesn't, so whoever is
reported isn't told, and a keeper can't tell whom it's about. Highlights,
pins and tags are the writer's own. What tracks the writer's own life
(`std.progress`, `std.checkin`, `std.visit`, `std.meal`, `std.time-entry`,
`std.reminder`) answers nobody.

## People as topics

Where a record names the people it is about, they are its topics:

- `did` on `std.award`, `std.follow` and `std.list-item`
- `from` and `to` on `std.settlement`
- `to` and `people` on `std.call`
- `paidBy` and `owedBy` on `std.expense`

A topic can't reach into a list of objects, so `std.expense` repeats the
people in `split` as `owedBy`. It holds at most 63, so that with `paidBy`
each one is tagged: a record carries at most 64 tags. See
[spec 02 §8.1](https://minca.dev/spec/02-records.md).

## Channels

A space with more than one conversation keeps `std.channel` records, sorted
by `position` then name. A message is `in` its channel.

```ts
import type { Query } from '@minca/core';

// A channel's messages
const messages: Query = { collection: 'std.message', where: { 'link:in': channel.key } };

// Each channel, with how many messages it holds
const channels: Query = {
  collection: 'std.channel',
  include: { messages: { rel: 'in', from: 'std.message', count: true } },
};
```

A message in no channel is in the space's own room, which a chat shows
first as "general". In the `community` template only moderators and admins
make or rename channels.

A channel is a label, not a boundary: every member reads every channel. For
something only some members may read, use a space of its own or a direct
message.

## Direct messages

A `std.direct` is sealed. Only the people in `to` and whoever wrote it can
read `data`. Write and read it through `node.direct`
([direct messages](https://minca.dev/records/direct-messages.md)).

A chat groups direct messages into conversations by everyone in them: `to`
plus the writer. One it can't open must show as unreadable, not disappear.
`to` is a topic, so "direct messages to me" can be an automation of the
account's own that a keeper matches unread.

## Positions

A `position` is a string that sorts where a record goes in a hand-made
order, by plain string comparison. Make one with `positionBetween`:

```ts
import { positionBetween } from '@minca/core/schemas';

positionBetween(above?.body?.position, below?.body?.position);
```

Either side may be missing, for the ends. Digits are `0–9a–z`, and a
position never ends in `0`, so there's always room between two. Equal
positions sort by key. A record without one goes at the end. Any string that
sorts correctly is valid.

## Places and areas

A place with a point also carries its `cells`: the geohash of `lat` and
`lon` at precisions 3, 4, 5 and 6, coarsest first. They're about 156 km,
39 km, 4.9 km and 1.2 km across. `cellsOf(lat, lon)` makes them.

For Grünerløkka in Oslo, `lat` 59.9226 and `lon` 10.7522 give
`["u4x", "u4xs", "u4xsu", "u4xsus"]`. Geohash here is Niemeyer's: longitude
and latitude bits interleaved, longitude first, five to a character of
`0123456789bcdefghjkmnpqrstuvwxyz`.

`std.event` and `std.listing` name `place.cells` as a topic, and `std.place`
names `cells`. So a keeper that can't read a private space can still hand
over "listings in these cells". Tags match only when equal, which is why
each size is written out instead of one long geohash.

### Finding what is nearby

`cellsAround(lat, lon, km)` gives the nine cells that cover a circle: the
point's own cell and the eight around it, at the finest precision whose
cells are at least `km` across. Match any of them, then let the exact
distance decide:

```json
{ "$or": [{ "place.cells": { "$contains": "u4xs" } }, { "place.cells": { "$contains": "u4xk" } }] }
```

`distanceKm` is haversine on a sphere of 6371.0088 km. `cellsAround` returns
null for a radius over about 150 km, or a circle around a pole. Then every
place is a candidate. A record written without cells is found only by its
point.

Nothing checks that `cells` agree with `lat` and `lon`. A peer only checks
that the tags match the cells. Apps that read the body use the point, so a
wrong cell only hides a record from area matches.

A listing usually means someone's home, so the example's marketplace keeps
only the part of town. It moves the point to the middle of its precision 6
cell and leaves out the street.

Finding places by name isn't part of the library. The example asks Photon,
which uses OpenStreetMap's data (set by `VITE_MINCA_PLACES`), and shows OpenStreetMap's credit beside the results.
See [spec 02 §8](https://minca.dev/spec/02-records.md).

## Changing a definition

Before v1 the library changes without migrations. A space that holds an
earlier definition keeps it until someone applies a blueprint that carries
the new one.

A field that becomes a topic tags only what is written after the change.
Older records carry no tags, so "mentions me" or "assigned to me" match only
from then on.

Recent changes:

- `std.attachment`, `std.photo`, `std.video`, `std.folder` and `std.album`
  became one `std.file`. Groups of files are `std.list`s.
- `std.booking` is a `std.claim` about a `std.slot`. `std.bookmark` is a
  `std.list-item`. `std.reference` is gone: any link refers.
- `std.message`'s `channel` link and `std.task`'s `column` link are now `in`.
- `std.vote` is for polls only. A proposal is decided by `std.ballot`s.
- `mentions`, `replyingTo`, `respondingTo` and `owedBy` were added, and the
  people fields became topics
  ([#105](https://github.com/leifriksheim/minca/issues/105)).

## Planned

> **Planned:** references to other spaces. `std.follow`'s `space` is a plain
> space id, and a repost or list item can only point within its own space
> ([#38](https://github.com/leifriksheim/minca/issues/38),
> [spec 02 §5.3](https://minca.dev/spec/02-records.md)).

> **Planned:** `format` in the schema dialect, to check `when`, `day` and
> `url` ([spec 02 §6.2](https://minca.dev/spec/02-records.md)).
