Skip to main content

Chat API

StatusImplementation
✅ Shippedsrc/feats/chats/

In-app messaging for approved groups. Chats are auto-created when a like group is approved — all group members plus all event authors become participants.

Message Sending

Message rows, read receipts, attachments, typing, and presence use Supabase directly with RLS and Realtime. REST provides read paths and chat lifecycle actions; there is no POST /messages endpoint.

See also: Chats model.

All HTTP endpoints require Authorization: Bearer <supabase-access-token>.

Endpoints​

MethodPathStatusDescription
GET/chats✅ ShippedList my chats with last message preview
GET/chats/:id✅ ShippedGet chat details with participants
GET/chats/:id/messages✅ ShippedPaginate messages (newest-first)
POST/chats/direct✅ ShippedCreate or reuse a direct chat
GET/chats/attachments/:attachmentId/download-url✅ ShippedDownload an attachment
POST/chats/:id/messages—Use Supabase Realtime
POST/chats/:id/hide✅ ShippedHide chat for caller
POST/chats/:id/unhide✅ ShippedRestore chat in caller list
POST/chats/:id/leave✅ ShippedLeave chat
DELETE/chats/:id✅ ShippedOrganizer soft-deletes chat
POST/chats/:id/restore✅ ShippedRestore chat

Chat types​

TypeCreated byStatus
approved_groupAuto-created on group approval✅ Shipped
event_authorsCreated with an event✅ Shipped
directPOST /chats/direct or mutual people like✅ Shipped

List chats​

GET /chats?cursor=xxx&limit=20
Authorization: Bearer <token>
QueryTypeDefaultDescription
cursorstring—Pagination cursor
limitnumber20Page size (1-100)
{
"data": [
{
"id": "uuid",
"type": "approved_group",
"createdAt": "2026-04-01T00:00:00Z",
"lastMessageAt": "2026-04-01T12:30:00Z",
"lastMessageContent": "See you there!",
"lastMessageSenderId": "uuid"
}
],
"nextCursor": null
}

Get chat​

GET /chats/:id
Authorization: Bearer <token>
{
"id": "uuid",
"type": "approved_group",
"createdAt": "2026-04-01T00:00:00Z",
"participantIds": ["uuid", "uuid", "..."]
}

Visibility: only chat participants can view.

List messages​

GET /chats/:id/messages?cursor=xxx&limit=30
Authorization: Bearer <token>
QueryTypeDefaultDescription
cursorstring—Pagination cursor
limitnumber30Page size (1-100)

Returns messages newest-first (descending createdAt). Each message may include attachments, a replyTo preview, and readBy user IDs. Message inserts, attachments, and read receipts are client-direct Supabase writes protected by RLS; typing and presence use a private Realtime channel.

{
"data": [
{
"id": "uuid",
"chatId": "uuid",
"senderId": "uuid",
"content": "Hello!",
"createdAt": "2026-04-01T12:30:00Z",
"updatedAt": "2026-04-01T12:30:00Z",
"deletedAt": null
}
],
"nextCursor": "base64-cursor"
}

Errors​

StatusReason
401Missing / invalid bearer token
403Not a participant of the chat
404Chat not found

Realtime — Supabase Realtime​

Message writes and real-time updates use Supabase Realtime channels. Direct chat creation requires a completed profile but does not require friendship; a pair has at most one direct chat. Attachments are uploaded through Media and attached by a client-direct database insert. Attachment downloads go through the signed REST endpoint above. Clients subscribe via @supabase/supabase-js:

const channel = supabase
.channel(`chat:${chatId}`)
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "messages", filter: `chat_id=eq.${chatId}` },
(payload) => {
switch (payload.eventType) {
case "INSERT": /* message.created */ break;
case "UPDATE": /* message.updated */ break;
case "DELETE": /* message.deleted */ break;
}
},
)
.subscribe();

Row-Level Security guarantees a user only receives messages for chats they are a participant of, even on the realtime stream.

Implementation notes​

Source: src/feats/chats/.

src/feats/chats/
├── chats.router.ts # GET handlers, auth middleware
├── chats.service.ts # Business logic, pagination
├── chats.schemas.ts # Zod request/response schemas
└── chats.constants.ts # Page size limits
  • The API bypasses RLS (uses service_role) — authorization is done in code by checking chat participation.
  • Cursor-based pagination uses base64-encoded {createdAt, id} tuples.
  • Default page size: 30 messages, max: 100.