Core Concepts
Permissions
The default participants-only model and the canRead / canWrite hooks.
By default, only the two participants of a conversation can read or write
it - enforced on every chat.api.* call and every HTTP route. You can loosen
or tighten that with two hooks.
The hooks
import { chatpack } from "@chatpack/core";
const chat = chatpack({
storage,
auth,
permissions: {
// May `user` read `conversation`? Default: participants only.
canRead: ({ user, conversation }) =>
conversation.participantIds.includes(user.id) || isSupportAgent(user.id),
// May `user` write to `conversation`? Default: participants only.
canWrite: ({ user, conversation }) =>
conversation.participantIds.includes(user.id) && !isBanned(user.id),
},
});Both hooks receive a PermissionContext:
Prop
Type
Hooks may be sync or async and must return a boolean. Returning false maps
to a 403 - FORBIDDEN_READ or FORBIDDEN_WRITE - both over HTTP and as a
thrown ChatpackError from chat.api.*.
What the hooks do and don't cover
- Sender-only edit/delete is separate. Even with
canWritereturningtrue, only the original sender can edit or delete a message - a non-sender gets403 NOT_MESSAGE_SENDER. This rule is not hook-overridable. - Read permission gates live delivery too. SSE events are only delivered for conversations the connected user may read - participation is re-checked server-side per event.
- Adapters never enforce permissions. Core validates before calling
storage. If you write a custom adapter, don't re-check permissions there -
and on hosted databases, make sure browser/anon clients can't read the
chatpack_*tables directly (see Custom adapters).
Common patterns
Support/admin read access - let a support role read any conversation:
permissions: {
canRead: async ({ user, conversation }) =>
conversation.participantIds.includes(user.id) ||
(await hasRole(user.id, "support")),
},Read-only archive - block writes to conversations you've flagged:
permissions: {
canWrite: ({ user, conversation }) =>
conversation.participantIds.includes(user.id) &&
conversation.metadata.archived !== true,
},Blocklists - deny writing to someone who blocked the sender:
permissions: {
canWrite: async ({ user, conversation }) => {
if (!conversation.participantIds.includes(user.id)) return false;
const other = conversation.participantIds.find((id) => id !== user.id)!;
return !(await isBlocked({ by: other, target: user.id }));
},
},