Users
One function tells the editor who people are. Comments, suggested edits, revision history, live presence and mentions all read names, avatars, titles and colours from it, so you write the lookup once and every feature shows the same person the same way.
Setup
import { createRichTextEditor } from '@better-rich-text/core';
import { createUsersPlugin } from '@better-rich-text/users';
createRichTextEditor('#editor', {
actorId: currentUser.id,
plugins: [
createUsersPlugin({
currentUserId: currentUser.id,
fetchUsers: async (ids, signal) => {
const response = await fetch(`/api/users?ids=${ids.map(encodeURIComponent).join(',')}`, { signal });
return response.json();
},
// Optional: powers @mentions in the document and in comment replies.
searchUsers: async (query, signal) => {
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`, { signal });
return response.json();
},
// Optional: people you already have, so they render without a request.
users: [currentUser],
}),
// comments, suggestions, revisions, mentions…
],
});Set actorId to the same id so edits, comments and suggestions are stamped with it. With the collaboration server, the authenticated identity from your authenticate adapter wins.
User record
interface User {
id: string;
name: string;
avatar?: string; // https:// or data:image/…;base64; anything else is ignored
description?: string; // title, team or email, shown under the name
color?: string; // caret, highlight and avatar colour; derived from the id when absent
custom?: Record<string, JSON>; // roles, profile links, anything else
}fetchUsers returns the users it knows. Ids it leaves out are shown as "Unknown user".
How lookups behave
Batched. Ids requested in the same tick go out in one
fetchUserscall (batchDelay, default 0 ms), split into chunks ofmaxBatch(default 100).Cached. Each id is fetched once for the editor's lifetime, or until
ttlmilliseconds pass.invalidate(id)forces a refetch.Never blocking.
get(id)returns at once: the cached user, or a placeholder withpending: truewhile the request runs. Views repaint when records arrive.Failure-safe. A rejected
fetchUserskeeps placeholders, reports throughonError, and retries on the next request.
API
const users = usersOf(editor); // UserDirectory | undefined
users.get('u-2'); // ResolvedUser, synchronously (may be a placeholder)
await users.lookup('u-2'); // ResolvedUser once fetched
await users.lookupMany(ids); // in the order given
await users.current(); // the current user
await users.search('sam'); // through searchUsers (empty when not configured)
users.prime([user]); // add or replace records, for example from a server push
users.invalidate('u-2'); // refetch on next use
const stop = users.subscribe((arrived) => repaint(arrived));ResolvedUser adds color (always set), initials (for avatars without images) and pending.
Commands (also available to AI agents as tools): users.lookup { ids } and users.current.
Hook: action users.resolved with the users that just arrived.
Which features use it
| Feature | Uses |
|---|---|
| Comments | Author names, avatars and titles on threads and replies; @ search in reply boxes |
| Suggested edits | Author names, colours for the suggestion highlight, review cards |
| Revision history | The author recorded on new versions and the name shown in the list |
| Presence | Caret labels, caret and selection colours, avatars in the collaborators bar |
| Mentions | @ search in the document when no @ source is configured |
Every feature works without the directory, using the names it stored when the comment, suggestion or version was made. Stored records always keep the user id, so names update everywhere when a person changes theirs.
Server endpoints
See Server for the endpoint shapes.