Messages and links

Inboxes and messages between people, and links between records.

Inboxes and messages

addressed and direct records carry a recipient, named on the call you already make. The recipient is server-stamped as record.audienceUserId, never a data field.

// addressed (owner → person): your server sends this, e.g. "Mark shipped"
await updates.create({ text: "Your order shipped 🎉" }, { for: userId })

// direct (person → person): DMs, group chats, sharing, requests
await messages.create({ text: "hey!" }, { for: otherUserId })

// a group chat: each message names the group (up to 20 people), one record they all read
await messages.create({ text: "Dinner at 8?" }, { for: [annaId, benId, chrisId] })

// The reader's side is list(): the server returns only their own inbox
const { records } = await updates.list({ sort: "newest", limit: 20 })
  • Same message for everyone → admin_write. A specific thing for a specific person → addressed.
  • An addressed record is sent from your Gemmein dashboard, a relay or your server (a secret key, naming the person with for). No app session sends one, yours included: the signed-in app is always a person reading their own inbox, and a send from it is 403 forbidden ("only the app owner sends addressed records").
  • For inboxes, use watch(): it polls list({ since }) every 10 s (5 s floor), hands you only what changed or was deleted, and sleeps while the tab is hidden. Never write a tight loop by hand. Read-state lives in the person's own private collection.
  • direct is messaging inside your app, and your Gemmein dashboard can read every message. Never present it as private or encrypted chat.
  • Group chats: a direct message can name up to 20 people. Each message is one record they all read, which only its author edits, and record.recipients lists them. Anyone in the group replies the same way, naming the others. On a reply-only collection, everyone named must have written to the sender first. For more people, use a shared collection, or send one addressed record each.
  • Who can start a conversation on a direct collection is its directInitiation setting, which your human picks on the collection's page in the dashboard's Data: "anyone" (the default), "invited" (reply-only: a person writes only to someone who has already written to them in this collection) or "off" (no one sends from the app; existing messages stay readable). Your dashboard and a secret key are never held to reply-only.
  • The error names the fix: invalid_audience (the recipient isn't a person in this app), reply_only (this collection only allows replying to people who wrote first), sends_disabled (you turned direct messages off for this collection).

Store another record's id in a field and it becomes a link. On collections that support links (community, shared, direct) you can embed the linked records on reads with expand:

const { records } = await posts.list({ expand: ["authorId"] })
records[0].expand.authorId          // the linked record, or null if gone (tombstone)

expand throws on private, public_read, and admin_write, because those rules have no link shape; join in memory there. Up to 3 fields per call.

Was this page helpful?

Last updated