minca

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.

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 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. See also actions and spec 02 §6.4.

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), and their own settings and reminders in the account registry, node.account.space() (the node).

See spec 02 §4.3 and §7.3.

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.

FragmentShape
whenstring 10–64, x-kind: "when": RFC 3339 (2026-03-01T18:30:00+01:00), or YYYY-MM-DD for a whole day
daystring of 10, x-kind: "day": YYYY-MM-DD
urlstring 1–2048, x-kind: "url"
timeZonestring 1–64: an IANA zone, Europe/Oslo
DIDstring 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)
blob{ hash, size, mime, name?, key?, salt? }, x-kind: "file": what records.addFile gives
image{ blob, alt? }, x-kind: "image", alt ≤ 2000
positionstring 1–200, x-kind: "position" (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.

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).

The tables are made from the definitions in @minca/core/schemas each time minca.dev is built, so they always match the code.

Annotations

NameWhat it isBodyLinksRules
std.reactionAn emoji reaction to any record.respondingTo DID; emoji string 1–16; topic respondingToabout → *, oneonePer: [@author, link:about, emoji]
std.commentA 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, respondingToabout → *, one; replyTo → std.comment, one; shares → *, one—
std.tagA 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–100about → *, oneonePer: [@author, link:about, label]
std.ratingA score from 1 to 5, one per person per thing, changed by rating again.score integer 1–5; review string ≤ 10000about → *, oneonePer: [@author, link:about]
std.highlightA passage marked in a text, with an optional note.quote string 1–10000; prefix string ≤ 500; suffix string ≤ 500; note string ≤ 10000about → *, one—
std.pinA record pinned for everyone by a moderator. To keep something for yourself, put it on a std.list.note string ≤ 500about → *, oneonePer: [link:about]
std.reportA report that a record breaks the rules, for moderators.reason spam/abuse/sexual/misleading/illegal/other; note string ≤ 2000about → *, oneonePer: [@author, link:about]
std.labelA moderator’s label that apps act on, like nsfw (blurred) or spoiler (hidden). Members label things with std.tag.value string 1–64about → *, oneonePer: [link:about, value]
std.claimSomeone 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 respondingToabout → *, oneonePer: [link:about]

People

NameWhat it isBodyLinksRules
std.profileHow 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-profileHow 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.cardAn 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.followFollowing a person: one per person followed.did DID; space string ≤ 256; topic did—onePer: [@author, did]
std.blockSomeone to hide and keep away. Keep it in your own space.did DID; until when 10–64—onePer: [@author, did]
std.muteSomeone to quiet for a while. Keep it in your own space.did DID; until when 10–64—onePer: [@author, did]
std.statusWhat someone is up to now: one per person.text string ≤ 280; emoji string ≤ 16; until when 10–64—onePer: [@author]

Messaging and publishing

NameWhat it isBodyLinksRules
std.messageA 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, replyingToin → std.channel, one; replyTo → std.message, one; shares → *, one—
std.channelA named conversation within a space.name string 1–100; topic string ≤ 500; position position 1–200——
std.directA 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.postA 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, replyingToreplyTo → std.post, one; root → std.post, one; shares → *, one—
std.repostPassing a record on as it is: one per person per record.respondingTo DID; topic respondingToabout → *, oneonePer: [@author, link:about]
std.articleLong-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 mentionsin → std.publication, one—
std.publicationA 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.docA 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-blockOne 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 mentionsin → std.doc, one; parent → std.doc-block, one—
std.wiki-pageA 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.noteA 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.callA 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

NameWhat it isBodyLinksRules
std.listA 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-itemOne 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 didin → std.list, one; about → *, one—

Files and media

NameWhat it isBodyLinksRules
std.fileAny 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.trackA 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 ≤ 36file → std.file, one—
std.playA 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–64about → std.track, one—

Time and planning

NameWhat it isBodyLinksRules
std.calendarA calendar of events.name string 1–200; color string ≤ 32——
std.eventSomething 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.cellsin → std.calendar, one—
std.rsvpWhether someone is coming to an event: one answer per person.respondingTo DID; status going/maybe/no; guests integer 0–100; note string ≤ 500; topic respondingToabout → std.event, oneonePer: [@author, link:about]
std.slotA 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 ≤ 1000in → std.calendar, one—
std.columnA 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–200in → std.project, one—
std.taskA 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 assigneesin → std.column, std.project, one; parent → std.task, one—
std.projectA 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-entryTime someone spent on something.start when 10–64; end when 10–64; note string ≤ 1000about → *, one—
std.reminderA 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 booleanabout → *, one—
std.habitSomething to do regularly.name string 1–200; schedule string ≤ 1000; target number ≥ 0; unit string ≤ 32; position position 1–200——
std.checkinA habit done on a day: one per person per day.date day 10–10; value number; note string ≤ 1000about → std.habit, oneonePer: [@author, link:about, date]

Places and travel

NameWhat it isBodyLinksRules
std.placeA 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.visitSomeone having been at a place.at when 10–64; note string ≤ 2000about → std.place, one—
std.tripA 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.locationWhere 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

NameWhat it isBodyLinksRules
std.recipeA 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.mealA planned meal: one per day per meal.date day 10–10; meal breakfast/lunch/dinner/snack; note string ≤ 1000about → std.recipe, oneonePer: [date, meal]
std.journal-entryWhat someone wrote about a day: one entry per day.date day 10–10; content string ≤ 200000; mood string ≤ 32—onePer: [@author, date]
std.measurementSomething 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.workoutA 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.workA 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.progressWhere 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 ≤ 2000about → std.work, oneonePer: [@author, link:about]

Money and trade

NameWhat it isBodyLinksRules
std.expenseSomething 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.settlementMoney 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-accountA 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.transactionMoney 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 booleanin → std.money-account, one—
std.listingSomething 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.orderAn 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 respondingToabout → std.listing, many—
std.order-updateA step in an order: accepted, paid, shipped, delivered, cancelled.respondingTo DID; status accepted/paid/shipped/delivered/cancelled/refunded; note string ≤ 2000; topic respondingToabout → std.order, one—

Community and governance

NameWhat it isBodyLinksRules
std.pollA question with options to vote on.question string 1–500; options string 1–200[]; closed boolean—fixed: [options]
std.voteOne 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 respondingToabout → std.poll, oneonePer: [@author, link:about]
std.proposalSomething 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.ballotOne 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 respondingToabout → std.proposal, oneonePer: [@author, link:about]; fixed: [choice]
std.decisionWhat 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, oneonePer: [link:about]; a contract
std.goalA 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.pledgeA promise toward a goal: once per person, not changed once made.respondingTo DID; amount integer 1–1000000000; note string ≤ 1000; topic respondingToabout → std.goal, oneonePer: [@author, link:about]; fixed: [amount]
std.goal-reachedA goal reached, proven by the pledges it cites.goal string 1–128; pledges string 1–128[] (≤ 256)about → std.goal, oneonePer: [link:about]; a contract
std.announcementNews for everyone in the space.title string 1–300; text string ≤ 20000——
std.badgeA 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.awardA badge given to someone, once.did DID; note string ≤ 1000; topic didabout → std.badge, oneonePer: [link:about, did]

Settings

NameWhat it isBodyLinksRules
std.settingA 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.hostA 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

NameWhat it isBodyLinksRules
std.automationWhen 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-runAn automation acted on a record: once each, whichever device did it.did string 1–500; ok boolean; at when 10–64; made string 1–256automation → std.automation, one; about → *, oneonePer: [link:automation, link:about]
std.activitySomeone at work on a record: working, waiting, done or failed.state working/waiting/done/failed; label string 1–120; at when 10–64about → *, oneonePer: [@author, link:about]

Bots

NameWhat it isBodyLinksRules
std.botHow 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.connectSomeone 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.connectorA 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

NameWhat it isBodyLinksRules
std.blueprintCollections 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.guideHow 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

NameWhat it isBodyLinksRules
std.contactSomeone 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-requestA knock left for someone in a space you share, sealed so only they can open it.to string ≤ 64; sealed string ≤ 16000——
std.inboxWhere 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.knockA 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-inviteAn 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). If the space defines std.call, a call leaves a record of its history there.

FieldTypeMeaning
status"missed" | "ended"Required.
tostring ≤ 256For a missed call: who was rung.
startedAtISO date, ≤ 64Required.
endedAtISO date, ≤ 64For an ended call.
peoplestring ≤ 256[] (≤ 64)For an ended call: every account seen in it.
{
  "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.
{ "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.

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 and spec 08 §2.

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.
  • 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.

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.

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.

NameBodyLinksRules
std.activitystate working/waiting/done/failed; label string ≤ 120; at when, since this stateabout → any, oneonePer: [@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.

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). 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).

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.

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.

{ "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.

{ "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:

{ "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 and spec 02 §8.

Responses

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

RecordrespondingTo
std.reaction, std.comment, std.repostthe author of what it is about
std.rsvpthe event's author
std.vote, std.ballotthe poll's or the proposal's author
std.pledgethe goal's author
std.claimthe claimed record's author
std.order, std.order-updatethe 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.

Channels

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

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).

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:

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:

{ "$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.

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.lists.
  • 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.ballots.
  • mentions, replyingTo, respondingTo and owedBy were added, and the people fields became topics (#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, spec 02 §5.3).

Planned: format in the schema dialect, to check when, day and url (spec 02 §6.2).

On this page