Messaging & Channels
Two parallel systems: direct messages between two people, and channels owned by creators containing rooms. They share no models. All endpoints require authentication.
Messages are sent over REST and delivered over the socket — see Realtime.
Direct messages
| Method | Path | Purpose |
|---|---|---|
POST | /conversation | Create or fetch a DM with a user |
GET | /conversation/inbox | List conversations |
GET | /conversation/:conversationId/messages | Message history |
POST | /conversation/:conversationId/messages | Send a message |
PUT | /conversation/:conversationId/messages/:messageId | Edit |
DELETE | /conversation/:conversationId/messages/:messageId | Delete (soft) |
POST | /conversation/:conversationId/read | Mark read |
POST | /conversation/:conversationId/leave | Leave |
POST /conversation
Create-or-get, not create. The service builds a deterministic dmKey from the two participant ids and upserts on it, so calling this repeatedly for the same pair always returns the same conversation.
Sending
{ "body": "hey", "type": "text" }type is "text" or "shared_post"; a shared post sets sharedPostId. This is what powers share-to-DM from the feed.
After persisting, the API calls esu:
POST {REALTIME_SERVICE_URL}/admin/emit/dm/message/new
X-Realtime-Admin-Token: …
{ "conversationId": "…", "message": { … } }which emits dm:message:new into the dm:<conversationId> room.
Deletes are soft
ConversationMessage.deletedAt is set rather than the row being removed, so clients can render "message deleted" in place.
Read state
POST /conversation/:id/read updates ConversationReadState (lastReadAt, lastReadMsgId) for the caller and emits dm:read to the other participant. Unread counts are derived from this.
Channels
| Method | Path | Purpose |
|---|---|---|
GET | /channels | List channels |
POST | /channels | Create (multipart/form-data, banner) |
GET | /channels/:channelId | One channel |
PUT | /channels/:channelId | Update (multipart/form-data) |
DELETE | /channels/:channelId | Delete |
POST | /channels/:channelId/join | Join |
A channel belongs to one creator and contains rooms. Joining creates a ChannelMember row — membership alone does not grant access to vip rooms; that requires an active subscription.
Rooms
| Method | Path | Purpose |
|---|---|---|
GET | /channels/:channelId/rooms | List rooms in a channel |
POST | /channels/:channelId/rooms | Create a room |
GET | /rooms/:roomId | One room |
PUT | /rooms/:roomId | Update |
DELETE | /rooms/:roomId | Delete |
GET | /rooms/:roomId/messages | Message history |
POST | /rooms/:roomId/messages | Send a message |
Room types and visibility
type | Meaning |
|---|---|
chat | Text |
voice | LiveKit audio |
video | LiveKit video |
e-date | 1-on-1 session room |
visibility | Who can enter |
|---|---|
public | Anyone |
members (default) | The owner or a ChannelMember |
vip | A member with an active CreatorSubscription to the owner |
private | Only the two parties of the attached Session |
Enforced by canAccessRoom in apps/api/src/lib/roomAccess.ts — the same function backs both the LiveKit token endpoint and the socket join authorization. See Livestreaming & Voice.
Voice and video rooms
| Method | Path | Purpose |
|---|---|---|
GET | /rooms/:id/token | Mint a LiveKit access token (403 if not permitted) |
POST | /rooms/:id/join | Record a ChannelRoomParticipant |
POST | /rooms/:id/leave | Remove the participant record |
GET /rooms/:id/token checks access, ensures the LiveKit room exists, and returns a token scoped to that room with a 1-hour TTL. The API is the only issuer of LiveKit tokens.
Interservice authorization
Used by esu, not by clients:
| Method | Path | Purpose |
|---|---|---|
GET | /dm/:conversationId/authz | May the caller join this DM? |
GET | /room/:roomId/authz | May the caller join this room? |
Both require the user's own bearer token; esu forwards it. Any non-2xx becomes AUTHZ_DENIED on the socket.
Realtime events
| Event | When |
|---|---|
dm:message:new | A DM is sent |
dm:message:edited / dm:message:deleted | Edit or soft delete |
dm:read | Read receipt |
dm:typing | Typing indicator |
room:message:new | Room message |
channel:room:created | A room is added to a channel |
Full contract, including the drift between the declared types and the wire format: Socket Events.
