Plugins
Conversations

Configuration

Every option of conversations(), with defaults and where each is explained.

import { conversations } from '@10x-media/conversations'

conversations({
  slug: 'comments',
  access,
  channels,
  // everything below is optional
})

Options

OptionTypeDefault
slugstringrequiredNames the instance: <slug>-messages, <slug>-reads, /api/conversations/<slug>. Lowercase letters, digits, dashes. See Concepts
accessConversationsAccessrequiredBatch conversation access; fails closed without it. See Access
channelsConversationsChannel[]requiredSlug, label, cue, access rules. See Access
targets{ collections?, globals?, custom? }{}Channels per target; extensions add to it. See Concepts
usersArray<string | ConversationsUsersConfig>[admin.user]Auth collections whose users take part, with an optional display projection
mentions.users({ req, key, channel, collection }) => Whereevery userWho can be mentioned, per users collection
mentions.maxnumber20Mentions kept per message
mentions.verifyAccessbooleanfalseAlso run conversation access as each candidate
hooks.afterMessage({ req, message, operation }) => voidAfter create, edit and delete. See Messages
hooks.afterMention({ req, message, users, key, channel }) => voidNew mentions only
editor({ defaultFeatures }) => featuresthe defaultsThe instance's server editor. See Rich text
typesMessageTypeDefinition[][]Custom message types, each with layout: 'message' | 'bare'. See Messages
systemAuthorsRecord<string, { name: LocalizedLabel; avatar? }>{}Authors that are not people, for postMessage({ author: { system } }). See System authors
limits.bodyLengthnumber10000Characters of the plain-text projection
limits.bodyBytesnumber65536Bytes of the serialized Lexical body
limits.dataBytesnumber16384Bytes of a custom type's serialized data
deleted'placeholder' | 'placeholderIfReplies''placeholderIfReplies'What a deleted message leaves behind. See Messages
deleteWithTargetboolean | ({ req, key, instance }) => voidtrueCascade when a target document is deleted; a function replaces the built-in cascade. See Production notes
readsbooleantruePer-user read state and unread counts
transportConversationsServerTransportpollingsseTransport(), pusherTransport() or your own. See Transport
slotsChatSlotConfigAdmin components added to the UI. See Slots
serverComponents'widget' | 'server-function''widget'How server slot and message type components reach the admin: an internal dashboard widget, or the plugin's own server function registered in app/(payload)/layout.tsx. See Slots
components{ Trigger?, Drawer?, Panel?, ChannelTabs?, Feed?, Message?, Composer?, Thread? }built-insClient component paths that replace built-in admin components. See Replacing components
overrides.messages, overrides.reads{ fields?, hooks?, admin?, access? }Change the collections. fields receives { defaultFields }. See Concepts
extensionsConversationsExtension[][]comments(), reactions(), attachments() and your own. See Extensions
translationsTranslationsOptionPer-locale string overrides. See i18n
disabledbooleanfalseReturns the config untouched

Slots

SlotWhere
composerAboveAbove the composer, under the channel cue
composerBelowInside the composer's box, under the text (not while editing)
composerActionsNext to Send (not while editing)
drawerHeaderIn the drawer header, after the title
messageQuickActionsA row at the top of each message's menu, for one-click actions such as reactions
messageActionsItems in each message's menu, after Reply in thread, Edit and Delete
messageFooterUnder each message body, deleted placeholders included

Each takes one component path or a list. Extensions append theirs after the host's.

Types

All exported from @10x-media/conversations (and @10x-media/conversations/types).

ConversationsChannel

type ConversationsChannel = {
  slug: string
  label: string | Record<string, string>
  cue?: ChannelCue | ((args: { req }) => ChannelCue | null)
  access: {
    read: (args: { channel: string; req }) => boolean | Promise<boolean>
    create: (args: { channel: string; req }) => boolean | Promise<boolean>
    update?: (args: { message; req }) => boolean | Promise<boolean> // default: the author
    delete?: (args: { message; req }) => boolean | Promise<boolean> // default: the author
  }
}

type ChannelCue = { label: string | Record<string, string>; tone: 'neutral' | 'warning' }

ConversationsAccess and TargetGrant

type ConversationsAccess = (args: {
  req: PayloadRequest
  targets: Array<{ key: string; kind: 'collection' | 'global' | 'custom'; slug: string; id?: string }>
}) => ConversationsAccessResult | Promise<ConversationsAccessResult>

type ConversationsAccessResult = string[] | Record<string, TargetGrant | false | null | undefined>

type TargetGrant = true | string[] | { read: true | string[]; create?: true | string[] }

perTarget(check, { load? }) builds a ConversationsAccess from a per-target check that returns false or a TargetGrant.

ConversationsUsersConfig

type ConversationsUsersConfig = {
  collection: string
  /** Default: the collection's useAsTitle, then email, then the id; no avatar. */
  display?: (doc: Record<string, unknown>) => { name: string; avatar?: string | null }
}

MessageTypeDefinition

type MessageTypeDefinition<TData> = {
  slug: string
  Component?: PayloadComponent // client or server component
  validate?: (data: TData) => true | string | Promise<true | string>
  clientCreatable?: boolean // default false
  layout?: 'message' | 'bare' // default 'message'; 'bare' draws the Component as the whole row
}

Declare one with defineMessageType<TData>()({ ... }) so data is typed.

ConversationsServerTransport

type ConversationsServerTransport = {
  publish: (args: { instance: string; key: string; channel?: string; req }) => void | Promise<void>
  endpoints?: (args: { base: string; instance: ConversationsInstance }) => Endpoint[]
  // what the admin uses; `factory` is the import path of your own client transport
  client?: 'polling' | 'sse' | { pusher: PusherClientOptions } | { factory: string; options?: unknown }
}

The built-in transports and buses are covered on Transport. The extension type, ConversationsExtension, is covered on Writing your own.

Validation at startup

The plugin throws while building the config, naming the instance, when:

  • the slug is malformed, or two instances share one;
  • channel slugs repeat, or a target lists an unknown channel;
  • a target collection or global does not exist;
  • a message type is called text, or two parties register one type slug;
  • two parties give one target different channel lists;
  • two extensions share a name, or an extension is named like a core or transport endpoint (subscribe, messages, read, poll, mentions, events, pusher-auth);
  • two extensions add a message field with one name, or one that the core already has.

On this page