Skip to content

AppView

The Colibri AppView is responsible for aggregating message data from across the network and providing endpoints for interacting with this data. Any requests to the AppView are meant to be proxied via the user’s PDS, to which the client application makes the requests. When requests are forwarded, the following DID URI should be used to indicate the target:

did:web:api.colibri.social#colibri_appview

The push-registration endpoints registerPush and unregisterPush instead target the notification service:

did:web:api.colibri.social#colibri_notif

Both services resolve to the same endpoint today, so this only affects which atproto-proxy fragment clients send; using #colibri_notif for push registration keeps the declared service boundary correct if the notification service is ever split onto its own deployment. The notification feed reads (listNotifications, getUnreadCount, updateSeen, updateSeenForMessage, getUnseen) remain on #colibri_appview.

The AppView supports a range of XRPC methods that PDSs can call. All com.atproto endpoints adhere to the original specification. There are also certain social.colibri XRPC endpoints which allow for extended functionality. All of these endpoints are available under the /xrpc/ path.

Resolves a DID to a DID document. Does not bi-directionally verify the handle.

GET/xrpc/com.atproto.identity.resolveDid
Unauthenticated
Query Parameters
did (did)
Required
Response
didDoc (DID Document)
Required

Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the DID document.

GET/xrpc/com.atproto.identity.resolveHandle
Unauthenticated
Query Parameters
handle (handle)
Required
Response
did (did)
Required

Resolves an atproto handle or DID to a DID document. Handles are resolved to a DID first, then the DID document is fetched.

GET/xrpc/com.atproto.identity.resolveIdentity
Unauthenticated
Query Parameters
identity (did | handle)
Required
Response
didDoc (DID Document)
Required

Proxies a blob fetch to the PDS that hosts the given DID. Blobs are not stored locally.

GET/xrpc/com.atproto.sync.getBlob
Unauthenticated
Query Parameters
did (did)
Required
cid (cid)
Required
Response
blob (bytes)
Required

Get a single cached record from the AppView.

GET/xrpc/com.atproto.sync.getRecord
Unauthenticated
Query Parameters
repo (at-identifier)
Required
collection (nsid)
Required
rkey (record-key)
Required
Response
uri (at-uri)
Required
value (object)
Required

List a range of cached records in a repository, matching a specific collection.

GET/xrpc/com.atproto.sync.listRecords
Unauthenticated
Query Parameters
repo (at-identifier)
Required
collection (nsid)
Required
limit (integer)
cursor (string)
reverse (boolean)
Response
cursor (string)
records (object[])
Required

Identifies the server as a Colibri AppView and reports its flavor and version. method=“GET” nsid=“social.colibri.server.describeServer” response=[object Object] />

software is always "colibri-appview". This is the field clients key on to decide whether a host is a Colibri AppView. flavor identifies the AppView build ("vanilla" for the stock AppView; forks may set any arbitrary string). version is the running crate version.

Get the status data and profile for a Colibri user.

Profile fields (displayName, avatar, banner, description) are sourced from the user’s social.colibri.actor.profile record. When that record has syncBluesky: true, the AppView instead serves these four fields live from the user’s app.bsky.actor.profile record (Bluesky stays the source of truth). If the user has no social.colibri.actor.profile record yet (not onboarded), the AppView falls back to app.bsky.actor.profile. The Colibri-only theme is always taken from the Colibri profile record, and isBot is always derived from the Bluesky self-label convention.

GET/xrpc/social.colibri.actor.getData
Unauthenticated
Query Parameters
identifier (did | handle)
Required
Response
did (did)
Required
handle (handle)
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
syncBluesky (boolean)
Required
theme (object)
accentColor (string)
gradient (object)
primary (string)
secondary (string)
bannerColor (string)
status (object)
Required
emoji (string)
text (string)
Required

Sets the online state for a Colibri user.

POST/xrpc/social.colibri.actor.setState
Requires Authentication
Query Parameters
state (string)
Required
Response
onlineState (string)
Required

Lists all communities the Colibri user is a part of (and not banned from).

GET/xrpc/social.colibri.actor.listCommunities
Requires Authentication
Response
communities (object[])
Required
name (string)
Required
picture (blob)
description (string)
categoryOrder (string[])
uri (at-uri)
Required
requiresApprovalToJoin (boolean)
Required
isLegacy (boolean)
isOwner (boolean)

isOwner reports whether the authenticated caller owns the community (user holding the “Owner” role).

Lists every channel or community the authenticated user has muted. Each entry is one social.colibri.actor.mute record, subject is the AT-URI of the muted channel or community.

GET/xrpc/social.colibri.actor.listMutes
Requires Authentication
Response
mutes (object[])
Required
uri (at-uri)
Required
subject (at-uri)
Required

Opens a connection with a WebSocket stream that transmits relevant events for the user.

WS/xrpc/social.colibri.sync.subscribeEvents
Requires Authentication

Unlike the HTTP endpoints, this WebSocket cannot be PDS-proxied: browsers expose no way to set an Authorization header (or any custom header) on a WebSocket. The token is therefore passed through the one handshake field a browser can control: the subprotocol list (Sec-WebSocket-Protocol).

The client mints a short-lived service auth token (aud: did:web:api.colibri.social, lxm: social.colibri.sync.subscribeEvents) and opens the socket offering two subprotocols, in order:

  1. colibri.auth.bearer: a fixed sentinel marking that the next entry is the token.
  2. the service-auth JWT itself.
new WebSocket(url, ["colibri.auth.bearer", token]);

A service-auth JWT is base64url ([A-Za-z0-9._-]), every character of which is a valid subprotocol token, so it passes through the handshake unmodified. The AppView reads the entry following the colibri.auth.bearer sentinel, verifies it, and echoes the colibri.auth.bearer sentinel back in the response Sec-WebSocket-Protocol header, per RFC 6455 a browser that offered a subprotocol fails the connection unless the server selects one, so this echo is required. The JWT itself is never echoed back.

The auth query parameter is still accepted as a transitional / local-dev fallback; when both are present, the subprotocol token wins.

The event stream can choose to transmit events to clients who have signed up to receive them. All events follow the same JSON data structure: they have a type (which is the same as the heading below), and an optional data field.

Sent when the AppView receives a heartbeat message from the client. This event carries no data.

Events of this type are sent to clients when a community has been updated or deleted. If the event is set to delete, only the community’s uri needs to be supplied.

data
{
event: 'upsert' | 'delete';
uri: string;
name?: string;
description?: string;
picture?: blob;
categoryOrder?: string[];
requiresApprovalToJoin?: boolean;
}

Events of this type are sent to clients when a member joins a community, has their roles changed, or leaves. member_event is broadcast to all connected clients (not just the subject). Clients should check community to scope the update to the right community, and check member.did against their own DID to detect when they themselves have been admitted or had their roles updated.

event When membership member memberDid
join A new member record is written (auto-admit or approveMembership) AT-URI of the user’s membership declaration, if available Always present Absent
roles_updated An existing member record is updated (e.g. a moderator changes roles) Absent Always present with the new roles array Absent
leave A member record is deleted (kick, ban, or self-leave). Sent to all remaining members Absent Absent Always present
data
{
event: 'join' | 'roles_updated' | 'leave';
community: string;
membership?: string;
member?: {
did: string;
handle: string;
roles: string[];
joinedAt: string;
nickname?: string;
data: {
displayName: string;
avatar?: blob;
banner?: blob;
description?: string;
isBot: boolean;
onlineState: string;
status: {
text: string;
emoji?: string;
};
};
};
/** DID of the member who left, only present on `leave` events. */
memberDid?: string;
}

Events of this type are sent to clients for changes to the moderator-facing pending-applications queue of a requiresApprovalToJoin community. Broadcast to all connected clients.

event When did / handle / createdAt / data
create A new social.colibri.membership is indexed for a closed community, or a kicked member’s original membership record is still on file and the community currently requires approval Always present
resolve The application was admitted via approveMembership Absent
dismiss A moderator hid the application from the active queue via dismissApplication (off-protocol) Absent
undismiss A dismissed application was restored via undismissApplication (off-protocol) Absent
data
{
event: 'create' | 'resolve' | 'dismiss' | 'undismiss';
community: string;
membership: string;
did?: string;
handle?: string;
createdAt?: string;
data?: {
displayName: string;
avatar?: blob;
banner?: blob;
description?: string;
isBot: boolean;
onlineState: string;
status: {
text: string;
emoji?: string;
};
};
}

Events of this type are sent to clients when a category has been created, updated, or deleted. When the event is delete, all data except the uri and event can be omitted.

data
{
event: 'upsert' | 'delete';
uri: string;
community?: string;
name?: string;
channelOrder?: string[];
}

Events of this type are sent to clients when a channel has been created, updated, or deleted. When the event is delete, all data except the uri and event can be omitted. On upsert, category is always present so clients can place the channel in the correct category without a follow-up fetch.

data
{
event: 'upsert' | 'delete';
uri: string;
community?: string;
category?: string;
name?: string;
description?: string;
type?: string;
ownerOnly?: boolean;
allowedRoles?: string[];
allowedMembers?: string[];
}

Events of this type are sent to clients when a role has been created, updated, or deleted. When the event is delete, all data except uri is omitted. On upsert, community, name, permissions, and position are always present.

data
{
event: 'upsert' | 'delete';
uri: string;
community?: string;
name?: string;
color?: string;
permissions?: string[];
position?: number;
hoisted?: boolean;
mentionable?: boolean;
}

Events of this type are sent to clients when a message has been sent, edited, or deleted. When the event is delete, all data except the uri and event can be omitted. On upsert, the author object is always present.

data
{
event: 'upsert' | 'delete';
uri: string;
channel?: string;
text?: string;
facets?: facet[];
createdAt?: string;
edited?: boolean;
parent?: string;
attachments?: { blob: blob; name?: string; }[];
author?: {
did: string;
handle: string;
data: {
displayName: string;
avatar?: blob;
banner?: blob;
description?: string;
isBot: boolean;
onlineState: string;
status: {
text: string;
emoji?: string;
};
};
};
}

Events of this type are sent to clients when a reaction has been added to or removed from a message.

data
{
event: 'added' | 'removed';
uri: string;
emoji?: string;
target?: string;
}

Events of this type are sent to clients when a user the client knows about has updated their Bluesky profile or Colibri status.

data
{
did: string;
status?: {
emoji?: string;
text: string;
state: 'online' | 'away' | 'dnd' | 'offline';
};
profile: {
displayName?: string;
avatar?: blob;
banner?: blob;
description?: string;
isBot: boolean;
handle: string;
};
}

Events of this type are sent to clients when a user is typing in the channel the user is currently viewing. For notes on how to report this to the AppView, see the view message type.

data
{
event: 'start' | 'stop';
channel: string;
did: string;
}

The following messages can be sent from the client to the AppView. All messages follow the same JSON data structure: they have a type (which is the same as the heading below), and an optional data field.

A generic event that carries no data. It is used to keep the socket connection alive. The AppView will always respond with an ack event.

Clients send a typing message to report that the current user is typing in a channel. The AppView broadcasts a typing_event to every other client that is currently viewing the same channel (determined by the last view message they sent). No response is sent back to the sender.

data
{
channel: string;
}

Clients can send a view message to the AppView to inform it about the channel the user is currently viewing. The AppView uses this to determine who should receive typing_event broadcasts.

data
{
channel: string;
}

Clients must send a voice_join message to the AppView when the user joins a voice channel.

data
{
channel: string;
community: string;
}

Clients must send a voice_leave message to the AppView when the user leaves a voice channel.

Mints a new community DID on the AppView’s own PDS and bootstraps a fully populated community. The AppView authenticates against PDS_LOC using admin credentials so it can create accounts without an invite code, stores the new account’s credentials encrypted in community_credentials, and writes five records to the new repo:

  • social.colibri.community (pinned at rkey: "self")
  • social.colibri.category: a default "General" category
  • social.colibri.channel: a default "general" text channel inside that category
  • social.colibri.role: an "Owner" role with every permission and protected: true so role-management endpoints refuse to delete it
  • social.colibri.member: subject = authenticated caller, holding the Owner role

The caller ends up as an owner-equivalent member of the new community. A community picture is uploaded by sending its raw bytes as the request body with the image’s MIME type declared via the mimeType query parameter. If supplied, the picture is validated against the allowed MIME types (image/jpeg, image/png, image/gif), uploaded to the community’s PDS, and stored as the community picture. An empty body means no picture.

POST/xrpc/social.colibri.community.create
Requires Authentication
Query Parameters
name (string)
Required
description (string)
requiresApprovalToJoin (boolean)
mimeType (string)
Request Body
image (bytes)
Response
did (did)
Required
community (at-uri)
Required
category (at-uri)
Required
channel (at-uri)
Required
ownerRole (at-uri)
Required
member (at-uri)
Required

The AppView identifies communities by DID; handles minted alongside the account are an implementation detail of PDS hosting and are not expected to be used for identity resolution. The AppView fails to start if any of PDS_LOC, APPVIEW_HANDLE_DOMAIN, or CREDENTIAL_ENCRYPTION_KEY are missing.

Updates the community’s settings. Only fields that are supplied are changed. A new community picture is uploaded by sending its raw bytes as the request body with the image’s MIME type declared via the mimeType query parameter. An empty body leaves the existing picture untouched.

POST/xrpc/social.colibri.community.update
Requires Authentication
Query Parameters
community (at-uri)
Required
name (string)
description (string)
mimeType (string)
requiresApprovalToJoin (boolean)
Request Body
image (bytes)
Response
uri (at-uri)
Required

Broadcasts community_event { event: "upsert", uri, name?, description?, picture?, requiresApprovalToJoin? }.

Deletes a community. Requires the caller to hold the community.delete permission. For AppView-managed communities (those minted via create) the entire PDS account is torn down via com.atproto.admin.deleteAccount, removing the repo and every record on it. For BYO communities the AppView does not administer the hosting PDS, so the account is left intact; the AppView only stops tracking it.

In both cases the stored credentials and every locally cached record for the community DID are removed, so the community no longer surfaces through any read endpoint.

POST/xrpc/social.colibri.community.delete
Requires Authentication
Query Parameters
community (at-uri)
Required
Response
did (did)
Required

social.colibri.community.registerCredentials

Section titled “social.colibri.community.registerCredentials”

Registers user-supplied credentials (PDS endpoint + identifier + app password) for an existing community DID hosted on a PDS the AppView doesn’t manage. The AppView verifies proof-of-control by performing a com.atproto.server.createSession against the supplied PDS.

POST/xrpc/social.colibri.community.registerCredentials
Requires Authentication
Query Parameters
did (did)
Required
pds (uri)
Required
identifier (at-identifier)
Required
password (string)
Required
Response
did (did)
Required
source (string)
Required

social.colibri.community.approveMembership

Section titled “social.colibri.community.approveMembership”

Admits a pending applicant to a closed community (requiresApprovalToJoin: true) by writing the community-side social.colibri.member record. Requires approval.manage permission.

On admission, broadcasts application_event and clears any dismissal row for the subject.

POST/xrpc/social.colibri.community.approveMembership
Requires Authentication
Query Parameters
membership (at-uri)
Required
Response
did (did)
Required
community (at-uri)
Required
member (at-uri)

Lists the pending join applications for a community that requires approval (requiresApprovalToJoin: true). A pending application is an indexed social.colibri.membership targeting the community whose author does not yet hold a social.colibri.member record on the community repo.

GET/xrpc/social.colibri.community.listApplications
Requires Authentication
Query Parameters
community (at-uri)
Required
Response
applications (object[])
Required
did (did)
Required
handle (handle)
Required
membership (at-uri)
Required
createdAt (string)
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required
dismissedApplications (object[])
Required
did (did)
Required

dismissedApplications entries share the exact same shape as applications.

social.colibri.community.dismissApplication

Section titled “social.colibri.community.dismissApplication”

Hides a pending join application from the active listApplications queue without resolving it. Tracked only in AppView storage. Requires approval.manage.

Broadcasts application_event { event: "dismiss", community, membership, did }.

POST/xrpc/social.colibri.community.dismissApplication
Requires Authentication
Query Parameters
community (at-uri)
Required
did (did)
Required
Response
did (did)
Required
community (at-uri)
Required

social.colibri.community.undismissApplication

Section titled “social.colibri.community.undismissApplication”

The inverse of dismissApplication. Requires approval.manage.

Broadcasts application_event { event: "undismiss", community, membership, did }.

POST/xrpc/social.colibri.community.undismissApplication
Requires Authentication
Query Parameters
community (at-uri)
Required
did (did)
Required
Response
did (did)
Required
community (at-uri)
Required

Creates a new category in a community and appends it to the community’s categoryOrder. Requires category.create permission.

POST/xrpc/social.colibri.category.create
Requires Authentication
Query Parameters
community (at-uri)
Required
name (string)
Required
Response
uri (at-uri)
Required

Broadcasts category_event { event: "upsert", uri, community, name, channelOrder: [] } and community_event { event: "upsert", uri, categoryOrder }.

Updates a category’s name. Requires category.update permission.

POST/xrpc/social.colibri.category.update
Requires Authentication
Query Parameters
category (at-uri)
Required
name (string)
Response
uri (at-uri)
Required

Broadcasts category_event { event: "upsert", uri, name }.

Deletes a category and removes it from the community’s categoryOrder. Requires category.delete permission.

POST/xrpc/social.colibri.category.delete
Requires Authentication
Query Parameters
category (at-uri)
Required
Response
uri (at-uri)
Required

Broadcasts category_event { event: "delete", uri } and community_event { event: "upsert", uri, categoryOrder }.

Creates a new channel inside a category and appends it to the category’s channelOrder. Requires channel.create permission.

allowedRoles and allowedMembers optionally seed the channel’s post allow-list at creation, letting a channel be born restricted (see channel.update for the allow-list semantics). Both are provided as repeated query-string values. The same hierarchy guard as channel.update applies to every seeded entry: a non-admin creator may only grant access to roles strictly below their own highest role and to members they outrank; the community owner is exempt. A violation returns Forbidden.

POST/xrpc/social.colibri.channel.create
Requires Authentication
Query Parameters
community (at-uri)
Required
category (at-uri)
Required
name (string)
Required
type (string)
Required
description (string)
allowedRoles (string[] (repeated))
allowedMembers (string[] (repeated))
Response
uri (at-uri)
Required

Broadcasts channel_event { event: "upsert", uri, community, category, name, type, allowedRoles, allowedMembers } and category_event { event: "upsert", uri, channelOrder }.

Updates a channel’s name, description, owner-only flag, and/or post restrictions. Requires channel.update permission.

allowedRoles and allowedMembers form an allow-list of who may post in the channel: when both are empty, every community member may post (the default). When either is non-empty, only the community owner, members holding one of the listed roles, or the listed member DIDs may post. ownerOnly: true takes precedence over both lists and restricts posting to the owner alone.

Because a non-empty allowedRoles/allowedMembers list cannot be distinguished from “no change requested” using the list itself, clearing a restriction back to “everyone may post” requires the corresponding clearAllowedRoles/clearAllowedMembers flag, since passing an empty list is a no-op.

Two hierarchy guards apply:

  • ownerOnly is owner-only to change. Holding channel.update is not enough. Only the community owner may flip this flag, since it overrides every other restriction (including the editor’s own access, if they’re not the owner).
  • allowedRoles/allowedMembers edits are scoped to the editor’s hierarchy. Only the entries that actually change (added or removed relative to the channel’s current lists) are checked, entries already present that aren’t touched are left alone. For each changed role, the editor’s highest role position must be strictly greater than that role’s position (so an editor can never add or remove their own top role, or anything at or above it). For each changed member, the editor must outrank that member by the same strictly-greater rule applied to roles, computed the same way as the moderation hierarchy check used by banUser/kickUser. The community owner is exempt from both checks. A violation returns Forbidden.
POST/xrpc/social.colibri.channel.update
Requires Authentication
Query Parameters
channel (at-uri)
Required
name (string)
description (string)
ownerOnly (boolean)
allowedRoles (string[] (repeated))
clearAllowedRoles (boolean)
allowedMembers (string[] (repeated))
clearAllowedMembers (boolean)
Response
uri (at-uri)
Required

Broadcasts channel_event { event: "upsert", uri, name?, description?, ownerOnly?, allowedRoles?, allowedMembers? }.

Deletes a channel and removes it from its parent category’s channelOrder. Requires channel.delete permission.

POST/xrpc/social.colibri.channel.delete
Requires Authentication
Query Parameters
channel (at-uri)
Required
Response
uri (at-uri)
Required

Broadcasts channel_event { event: "delete", uri } and category_event { event: "upsert", uri, channelOrder }.

Persists a new channel order within a category. channelOrder is provided as repeated query-string values. Requires channel.update permission.

POST/xrpc/social.colibri.community.reorderChannels
Requires Authentication
Query Parameters
category (at-uri)
Required
channelOrder (string[] (repeated))
Required
Response
uri (at-uri)
Required

Broadcasts category_event { event: "upsert", uri, channelOrder }.

social.colibri.community.reorderCategories

Section titled “social.colibri.community.reorderCategories”

Persists a new category order for the community sidebar. categoryOrder is provided as repeated query-string values. Requires category.update permission.

POST/xrpc/social.colibri.community.reorderCategories
Requires Authentication
Query Parameters
community (at-uri)
Required
categoryOrder (string[] (repeated))
Required
Response
uri (at-uri)
Required

Broadcasts community_event { event: "upsert", uri, categoryOrder }.

Removes a member from the community by DID. Writes a kick moderation record and revokes the member record. Unlike kickUser, accepts a DID directly (no handle resolution). Requires member.kick permission.

POST/xrpc/social.colibri.community.kick
Requires Authentication
Query Parameters
community (at-uri)
Required
member (did)
Required
Response
did (did)
Required
handle (handle)
Required

Broadcasts member_event { event: "leave", community, memberDid } via the tap firehose once the member record is deleted.

Replaces a member’s full role set. roles is provided as repeated query-string values (AT-URIs or bare rkeys). Requires role.manage permission.

POST/xrpc/social.colibri.community.setMemberRoles
Requires Authentication
Query Parameters
community (at-uri)
Required
member (did)
Required
roles (string[] (repeated))
Required
Response
did (did)
Required
roles (string[])
Required

Broadcasts member_event { event: "roles_updated", community, member: { did, roles, data... } } via the tap firehose once the member record is updated.

Creates a new role in a community. Requires role.manage permission. position sets the role’s hierarchy rank; higher values sit higher. permissions is provided as repeated query-string values.

POST/xrpc/social.colibri.role.create
Requires Authentication
Query Parameters
community (at-uri)
Required
name (string)
Required
position (integer)
Required
permissions (string[] (repeated))
color (string (hex #rrggbb))
hoisted (boolean)
mentionable (boolean)
Response
uri (at-uri)
Required

Broadcasts role_event { event: "upsert", uri, community, name, permissions, position, color?, hoisted?, mentionable? } via the tap firehose once the role record is written.

Updates a role’s fields. Only supplied fields are changed; omitted fields keep their current values. Supplying a non-empty permissions list replaces the current permission set. Requires role.manage permission. Returns InvalidRequest if the role has protected: true.

POST/xrpc/social.colibri.role.update
Requires Authentication
Query Parameters
role (at-uri)
Required
name (string)
color (string (hex #rrggbb))
permissions (string[] (repeated))
position (integer)
hoisted (boolean)
mentionable (boolean)
Response
uri (at-uri)
Required

Broadcasts role_event { event: "upsert", uri, community, name, permissions, position, ... } via the tap firehose once the role record is updated.

Deletes a role from a community. Requires role.manage permission. Returns InvalidRequest if the role has protected: true (e.g. the bootstrap Owner role).

POST/xrpc/social.colibri.role.delete
Requires Authentication
Query Parameters
role (at-uri)
Required
Response
uri (at-uri)
Required

Broadcasts role_event { event: "delete", uri } via the tap firehose once the role record is deleted.

Lists all banned users for a community, each hydrated into their full profile (handle, Bluesky profile, and Colibri status).

GET/xrpc/social.colibri.community.listBannedUsers
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
users (object[])
Required
did (did)
Required
handle (handle)
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required

Lists all categories in a community.

GET/xrpc/social.colibri.community.listCategories
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
categories (object[])
Required
uri (at-uri)
Required
name (string)
Required
channelOrder (string[])
Required

Lists all channels in a community.

GET/xrpc/social.colibri.community.listChannels
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
channels (object[])
Required
uri (at-uri)
Required
name (string)
Required
type (string)
Required
category (string)
Required
description (string)
ownerOnly (boolean)
allowedRoles (string[])
allowedMembers (string[])

Lists all roles cached for a community.

GET/xrpc/social.colibri.community.listRoles
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
roles (object[])
Required
uri (at-uri)
Required
name (string)
Required
color (string)
permissions (string[])
Required
position (integer)
Required
hoisted (boolean)
mentionable (boolean)
protected (boolean)
channelOverrides (object[])
Required
channel (string)
Required
allow (string[])
Required
deny (string[])
Required

Lists all members with their roles, status information and Bluesky profiles.

GET/xrpc/social.colibri.community.listMembers
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
members (object[])
Required
did (did)
Required
handle (handle)
Required
roles (string[])
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required

Returns all cached data for a community in a single response: the community record, its categories, channels, roles, and members. Returns NotFound if no community record is cached for the given URI.

GET/xrpc/social.colibri.community.getData
Unauthenticated
Query Parameters
community (at-uri)
Required
Response
community (object)
Required
uri (at-uri)
Required
name (string)
Required
description (string)
Required
picture (blob)
categoryOrder (string[])
Required
requiresApprovalToJoin (boolean)
Required
categories (object[])
Required
uri (at-uri)
Required
name (string)
Required
channelOrder (string[])
Required
channels (object[])
Required
uri (at-uri)
Required
name (string)
Required
type (string)
Required
category (string)
Required
description (string)
ownerOnly (boolean)
allowedRoles (string[])
allowedMembers (string[])
roles (object[])
Required
uri (at-uri)
Required
name (string)
Required
color (string)
permissions (string[])
Required
position (integer)
Required
hoisted (boolean)
mentionable (boolean)
protected (boolean)
channelOverrides (object[])
Required
channel (string)
Required
allow (string[])
Required
deny (string[])
Required
members (object[])
Required
did (did)
Required
handle (handle)
Required
roles (string[])
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required

Hides a message in a given community. Requires the caller to hold the message.hide permission.

POST/xrpc/social.colibri.community.blockMessage
Requires Authentication
Query Parameters
community (at-uri)
Required
message (at-uri)
Required
Response
message (at-uri)
Required

Broadcasts message_event { event: "delete", uri: <message> } via the tap firehose once the moderation record is indexed, so connected clients remove the hidden message without a reload.

Bans a user from a community by writing a ban moderation record. Requires the member.ban permission. Callers can only ban members ranked strictly below them in the role hierarchy.

POST/xrpc/social.colibri.community.banUser
Requires Authentication
Query Parameters
community (at-uri)
Required
identifier (did | handle)
Required
Response
did (did)
Required
handle (handle)
Required

Unbans a user from a community, allowing them to write messages there again. Requires the member.unban permission.

POST/xrpc/social.colibri.community.unbanUser
Requires Authentication
Query Parameters
community (at-uri)
Required
identifier (did | handle)
Required
Response
did (did)
Required
handle (handle)
Required

Broadcasts member_event { event: "leave", community, memberDid } via the tap firehose once the member record is revoked.

Kicks a user from a community by writing a kick moderation record and revoking their membership. Unlike a ban, the kicked user is not prevented from rejoining. Requires the member.ban permission. Callers can only kick members ranked strictly below them in the role hierarchy.

POST/xrpc/social.colibri.community.kickUser
Requires Authentication
Query Parameters
community (at-uri)
Required
identifier (did | handle)
Required
Response
did (did)
Required
handle (handle)
Required

Broadcasts member_event { event: "leave", community, memberDid } via the tap firehose once the member record is revoked.

Creates an invitation code for the specified community. Requires the invitation.create permission.

POST/xrpc/social.colibri.community.createInvitation
Requires Authentication
Query Parameters
community (at-uri)
Required
Response
code (string)
Required
community (at-uri)
Required
createdBy (did)
Required
active (boolean)
Required

Get information about a given invitation code. Does not require authentication.

GET/xrpc/social.colibri.community.getInvitation
Unauthenticated
Query Parameters
code (string)
Required
Response
code (string)
Required
community (at-uri)
Required
createdBy (did)
Required
active (boolean)
Required

List all invitations for the specified community. Requires the invitation.create permission.

POST/xrpc/social.colibri.community.listInvitations
Requires Authentication
Query Parameters
uri (at-uri)
Required
Response
codes (object[])
Required
code (string)
Required
community (at-uri)
Required
createdBy (did)
Required
active (boolean)
Required

Deactivates an invitation code for the specified community. Requires the invitation.delete permission.

POST/xrpc/social.colibri.community.deleteInvitation
Requires Authentication
Query Parameters
uri (at-uri)
Required
code (string)
Required
Response
code (string)
Required

Get a paginated message history for the specified channel, newest messages first. Banned users’ messages are filtered out, as are messages that have been hidden by a hideMessage moderation action (and not later un-hidden). Hidden messages are excluded from both the top-level list and any embedded parent. Set all to true to include hidden messages in the response (e.g. for moderation views); it defaults to false. The createdAt field reflects when the message was authored (taken directly from the AT Protocol record), not when the AppView indexed it.

GET/xrpc/social.colibri.channel.listMessages
Unauthenticated
Query Parameters
channel (at-uri)
Required
limit (integer)
cursor (string)
all (boolean)
Response
cursor (string)
messages (object[])
Required
uri (at-uri)
Required
text (string)
Required
facets (Facet[])
Required
channel (at-uri)
Required
community (at-uri)
Required
author (object)
Required
did (did)
Required
handle (handle)
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required
parent (object)
uri (at-uri)
Required
text (string)
Required
facets (Facet[])
Required
channel (at-uri)
Required
community (at-uri)
Required
author (object)
Required
did (did)
Required
handle (handle)
Required
data (object)
Required
displayName (string)
Required
avatar (blob)
banner (blob)
description (string)
isBot (boolean)
Required
onlineState (string)
Required
status (object)
Required
emoji (string)
text (string)
Required
attachments (object[])
Required
blob (blob)
Required
name (string)
reactions (object[])
Required
emoji (string)
Required
count (integer)
Required
reactorDids (string[])
Required
createdAt (datetime)
Required
edited (boolean)
Required
attachments (object[])
Required
blob (blob)
Required
name (string)
reactions (object[])
Required
emoji (string)
Required
count (integer)
Required
reactorDids (string[])
Required
createdAt (datetime)
Required
edited (boolean)
Required

Get the read cursor for the current user and specified channel.

GET/xrpc/social.colibri.channel.getReadCursor
Requires Authentication
Query Parameters
channel (at-uri)
Required
Response
uri (at-uri)
Required
cursor (string)
Required
channel (at-uri)
Required

Returns per-channel unread status for the authenticated user across every channel in a community.

GET/xrpc/social.colibri.channel.listUnreadStatus
Requires Authentication
Query Parameters
community (at-uri)
Required
Response
channels (object[])
Required
channelUri (at-uri)
Required
hasUnreadMessages (boolean)
Required
unreadPingCount (integer)
Required

Get all reactions for a specified message, grouped by emoji with reactor DIDs.

GET/xrpc/social.colibri.channel.listReactions
Unauthenticated
Query Parameters
message (at-uri)
Required
Response
reactions (object[])
Required
emoji (string)
Required
count (integer)
Required
reactorDids (string[])
Required

Fetches a URL server-side and returns its OpenGraph/Twitter-card metadata, so the client’s IP is never exposed to the target site. Requires authentication to keep the AppView from becoming a public open URL-fetch proxy. Results are cached, including empty ones, to avoid re-fetching pages that carry no embed tags. The outbound request is guarded against SSRF (private, loopback, and link-local addresses are refused). largeImage reflects whether the preview should render large or as a small thumbnail.

GET/xrpc/social.colibri.embed.getMetadata
Requires Authentication
Query Parameters
uri (uri)
Required
Response
title (string)
description (string)
siteName (string)
themeColor (string)
image (object[])
url (uri)
Required
alt (string)
largeImage (boolean)

Proxies a remote embed preview image through the AppView so the client’s IP is never exposed to the image host. Unauthenticated, like com.atproto.sync.getBlob, because it is loaded directly via an <img src>, but constrained by the same SSRF guard and an image/* content-type requirement: anything that isn’t an image is rejected.

GET/xrpc/social.colibri.embed.getImage
Unauthenticated
Query Parameters
url (uri)
Required
Response
image (bytes)
Required

social.colibri.notification.listNotifications

Section titled “social.colibri.notification.listNotifications”

Returns a paginated list of notifications for the authenticated user, newest first. Each notification embeds the underlying message body so clients can render the notification without a follow-up fetch. message is omitted if the underlying message has been removed from the AppView cache.

GET/xrpc/social.colibri.notification.listNotifications
Requires Authentication
Query Parameters
limit (integer)
cursor (string)
Response
cursor (string)
notifications (object[])
Required
id (integer)
Required
recipientDid (did)
Required
kind (string)
Required
messageUri (at-uri)
Required
authorDid (did)
Required
channelUri (at-uri)
Required
indexedAt (datetime)
Required
seenAt (datetime)
message (object)
text (string)
Required
facets (Facet[])
Required
createdAt (datetime)
Required
parent (record-key)
attachments (object[])
Required
blob (blob)
Required
name (string)
edited (boolean)

The kind value is one of mention or reply.

Mentions and replies are deduplicated per (recipient, message, kind), so a single message that both mentions and replies to the recipient produces two notification rows.

social.colibri.notification.getUnreadCount

Section titled “social.colibri.notification.getUnreadCount”

Returns the count of unseen notifications for the authenticated user.

GET/xrpc/social.colibri.notification.getUnreadCount
Requires Authentication
Response
count (integer)
Required

Marks every unseen notification for the authenticated user whose indexedAt is at or before seenAt as seen. If seenAt is omitted, the AppView uses its current clock for both the cutoff and the stamped seenAt.

POST/xrpc/social.colibri.notification.updateSeen
Requires Authentication
Query Parameters
seenAt (datetime)
Response
updated (integer)
Required

Returns the authenticated user’s unseen notifications within a single channel. Clients use this when a channel opens to learn which messages still owe a ping, so each one can be cleared (via updateSeenForMessage) as its message scrolls into view.

GET/xrpc/social.colibri.notification.getUnseen
Requires Authentication
Query Parameters
channel (at-uri)
Required
Response
notifications (object[])
Required
id (integer)
Required
messageUri (at-uri)
Required
indexedAt (datetime)
Required

social.colibri.notification.updateSeenForMessage

Section titled “social.colibri.notification.updateSeenForMessage”

Marks the authenticated user’s unseen notifications for a single message as seen. When at least one row is cleared, the AppView broadcasts a seen_event with event: "message_seen" to the user’s other connected clients so their unread badges update live.

POST/xrpc/social.colibri.notification.updateSeenForMessage
Requires Authentication
Query Parameters
message (at-uri)
Required
Response
updated (integer)
Required

Registers a Web Push subscription for the authenticated user so the AppView can deliver background push notifications when the app is closed.

POST/xrpc/social.colibri.notification.registerPush
Requires Authentication
Request Body
platform (string)
Required
endpoint (string)
Required
keys (object)
Required
p256dh (string)
Required
auth (string)
Required
Response
registered (boolean)
Required

social.colibri.notification.unregisterPush

Section titled “social.colibri.notification.unregisterPush”

Drops a previously registered Web Push subscription for the authenticated user, identified by its endpoint. Called when the user disables notifications or the browser rotates/expires the subscription.

POST/xrpc/social.colibri.notification.unregisterPush
Requires Authentication
Request Body
endpoint (string)
Required
Response
unregistered (boolean)
Required

Notification events are emitted over social.colibri.sync.subscribeEvents immediately after a notification is indexed. The payload mirrors a single entry of listNotifications and always carries the message body.

data
{
id: integer;
kind: 'mention' | 'reply';
messageUri: string;
authorDid: string;
channelUri: string;
indexedAt: string;
message: {
text: string;
facets: facet[];
createdAt: string;
parent?: string;
attachments: { blob: blob; name?: string; }[];
edited?: boolean;
};
}

Alongside each notification_event broadcast over the WebSocket, the AppView also sends a VAPID Web Push message to every Web Push subscription the recipient has registered via registerPush.

The push payload is JSON shaped for the Service Worker that renders it:

push payload
{
title: string; // "New mention" | "New reply"
body: string; // the message text
tag?: string; // the message URI (collapses duplicates)
icon?: string;
data: {
channelUri: string; // required
messageUri: string;
};
}

The AppView owns the VAPID keypair and exposes the public key to clients out of band (as the PUBLIC_VAPID_KEY build-time environment variable). Subscriptions that the push service reports as gone (HTTP 404/410) are pruned automatically.

Emitted over social.colibri.sync.subscribeEvents to keep unread state in sync across a user’s devices. Unlike most events, a seen_event is delivered only to the originating user’s own connected clients (filtered by DID).

data
{
event: 'channel_read' | 'message_seen';
channelUri: string;
messageUri?: string; // present on message_seen
cleared?: number; // present on message_seen
}

Colibri is built to be self-hostable, so it’s possible that a community’s members are spread across several AppViews. On-protocol data reaches every AppView through the tap firehose. Off-protocol signals, like online status, typing, voice presence, never touch a repo, so without Humming they are contained within the AppView that produced them. A user on one instance can’t see the presence of a community member on another.

Humming bridges that gap. A Hum is a single off-protocol event (a status change, a typing start, a voice join/leave) that one AppView relays to another so it can be delivered to that instance’s clients over its ordinary social.colibri.sync.subscribeEvents stream. Humming carries only these ephemeral signals. It cannot carry a message, a membership, or any other on-protocol data.

Every community already has exactly one AppView that administers it, the holder of its community_credentials, the only party that can write to the community repo. That AppView is the community’s presence hub. It is discovered from the community record itself: social.colibri.community carries an appview field (a did:web) naming the home AppView.

Presence flows through the hub in a star topology, not a gossip mesh:

  1. A leaf (any AppView with a local, online member of the community) sends the member’s Hums to the community’s hub via sendHum.
  2. The hub injects the Hum for its own local clients and fans it out to every leaf subscribed via subscribeHums.
  3. Leaves never re-forward. A Hum received over subscribeHums is delivered to local clients only.

A Hum therefore makes at most two hops (leaf → hub → leaf), and an instance only ever sees traffic for communities its own users belong to. There is no whole-network flood.

A Hum is only acted on if it passes every check below. The design goal is that a forged or abusive Hum can, at worst, cause a cosmetic presence blip for a user who already opted in.

  1. Off-protocol data only. The wire type for a Hum’s event is a closed union of userEvent, typingEvent, and voicePresenceEvent. An on-protocol payload will be ignored.
  2. The peer AppView is authenticated. Every Hum carries an inter-service auth JWT signed by the sending AppView’s key (iss = its did:web, aud = the receiver’s did:web, lxm = the method NSID). This is a peer-to-peer AppView token, not the user OAuth token used by the rest of the API.
  3. The origin must be an AppView. The verified issuer must be a did:web. This stops a user from minting a service-auth token under their own did:plc and originating presence for themselves.
  4. The origin must be the subject’s declared presence service. A Hum about a user is dropped unless its origin equals that user’s presenceService (see Opting in). A valid signature proves who sent the Hum, this check proves who they may speak for. A missing presenceService means the user hasn’t opted in, so the Hum is dropped.
  5. The subject must be a member. The subject must hold a social.colibri.member record on the target community, so a peer can’t inject a user into a community they never joined.
  6. Channels must belong to the community. Any channel a typing or voice event references must be an at-uri whose authority is the target community.

Finally, the receiver re-derives the subject’s identity from its own view of that user’s repo and attributes the injected event to the envelope’s subject. The peer-supplied identity fields inside the event payload (display name, handle, the inner did) are discarded and only the ephemeral status/typing/voice signal is taken from the Hum. This closes any attempt to attribute an event to a different user.

Cross-instance presence is opt-in per user. A user opts in by publishing a presenceService field (a did:web) on their social.colibri.actor.profile, naming the AppView allowed to speak for their presence. The reference Colibri client keeps this in sync with the AppView the user actually connects to (the one chosen in the settings picker) and exposes a “Share presence across AppViews” toggle, on by default.

Turning the toggle off removes the presenceService key from the profile. A missing key is the signal for “opted out”: every AppView drops Hums about that user, so their presence, typing, and voice never cross an instance boundary. Local, same-AppView presence is unaffected.

Both endpoints exchange the same envelope:

humEnvelope
{
origin: string; // did:web of the AppView that produced the Hum
id: string; // unique per origin, used for dedup across relay paths
ttl: number; // remaining hops (0..8), starts at 1, hub decrements to 0
subject: string; // did of the user the event is about
community: string; // at-uri of the community the event is scoped to
event: UserEvent | TypingEvent | VoicePresenceEvent;
}

userEvent and typingEvent are the same shapes emitted over subscribeEvents. voicePresenceEvent describes a voice channel join/leave:

voicePresenceEvent
{
event: 'join' | 'leave';
channel: string; // at-uri of the voice channel
did: string; // subject (re-derived by the receiver)
}

Informs this AppView, acting as a community’s hub, of an off-protocol event that occurred for one of the calling AppView’s users.

POST/xrpc/social.colibri.sync.sendHum
Unauthenticated
Request Body
origin (did)
Required
id (string)
Required
ttl (integer)
Required
subject (did)
Required
community (at-uri)
Required
event (object)
Required

sendHum is authenticated with a peer-AppView inter-service auth JWT, not the user OAuth token used elsewhere. The token is supplied as a bearer token (Authorization: Bearer <jwt>), with aud set to the receiving AppView’s did:web and lxm set to social.colibri.sync.sendHum. The receiver resolves the issuer’s DID document, verifies the signature, and treats iss as the peer AppView identity, which must equal the envelope’s origin.

The receiver then runs the trust model checks, injects the event locally (presence and voice as Community-scoped events on subscribeEvents, typing as a client-to-client typing event), and, if it is a hub, while ttl > 0, decrements ttl and republishes the Hum to its subscribeHums subscribers.

Errors: AuthRequired (missing/invalid token), Forbidden (any trust-model check failed), RateLimited (peer exceeded its per-peer budget), UnsupportedEvent (payload was not an off-protocol event), NotEnabled (Humming disabled on this AppView).

A peer AppView opens this WebSocket to receive, in real time, the Hums this AppView relays in its hub role.

WS/xrpc/social.colibri.sync.subscribeHums
Unauthenticated

Like subscribeEvents, a browser-incompatible header can’t be set on a WebSocket, so the peer’s inter-service auth JWT (aud = this AppView, lxm = social.colibri.sync.subscribeHums) is offered through the subprotocol list, after the same colibri.auth.bearer sentinel:

new WebSocket(url, ["colibri.auth.bearer", token]);

The sentinel is echoed back on success, the token is not.

A valid token authenticates who is connecting, not what they may see. Streaming every relayed Hum to any authenticated peer would leak the presence of every user in every community on the hub. Instead, the hub only forwards Hums for a community when that community has a member who declared the connecting peer as their presenceService. This authorized set is recomputed periodically as memberships and presenceService choices change.

A peer may additionally narrow what it receives by declaring the communities it cares about as repeated communities query parameters (?communities=<did>&communities=<did>). This is intersected with the authorized set: it can only shrink the stream, never widen it. Omitting it streams everything the peer is authorized for.

The star topology bounds propagation structurally (two hops, no re-forwarding), and several additional guards protect self-hosted instances:

  • ttl starts at 1 and the hub decrements it to 0 on rebroadcast; a bound that, combined with leaves never re-forwarding, prevents loops.
  • Dedup. Each AppView keeps a bounded set of recently-seen envelope ids and drops duplicates that arrive over more than one relay path, along with any Hum it originated itself.
  • HUM_MAX_PEERS caps the number of distinct remote hubs a leaf will dial (default 32). Beyond the cap, extra hubs are skipped.
  • HUM_MAX_SUBSCRIBERS caps concurrent inbound subscribeHums streams a hub will hold (default 256). Further connections are rejected with TooManySubscribers.
  • Per-peer rate limit on sendHum (HUM_RATE_LIMIT, default 240/minute per peer). Presence and typing are inherently low-rate, so a peer exceeding the budget is RateLimited.