Chat API
| Status | Implementation |
|---|---|
| ✅ Shipped | src/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 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
| Method | Path | Status | Description |
|---|---|---|---|
| GET | /chats | ✅ Shipped | List my chats with last message preview |
| GET | /chats/:id | ✅ Shipped | Get chat details with participants |
| GET | /chats/:id/messages | ✅ Shipped | Paginate messages (newest-first) |
| POST | /chats/direct | ✅ Shipped | Create or reuse a direct chat |
| GET | /chats/attachments/:attachmentId/download-url | ✅ Shipped | Download an attachment |
| POST | /chats/:id/messages | — | Use Supabase Realtime |
| POST | /chats/:id/hide | ✅ Shipped | Hide chat for caller |
| POST | /chats/:id/unhide | ✅ Shipped | Restore chat in caller list |
| POST | /chats/:id/leave | ✅ Shipped | Leave chat |
| DELETE | /chats/:id | ✅ Shipped | Organizer soft-deletes chat |
| POST | /chats/:id/restore | ✅ Shipped | Restore chat |
Chat types
| Type | Created by | Status |
|---|---|---|
approved_group | Auto-created on group approval | ✅ Shipped |
event_authors | Created with an event | ✅ Shipped |
direct | POST /chats/direct or mutual people like | ✅ Shipped |
List chats
GET /chats?cursor=xxx&limit=20
Authorization: Bearer <token>
| Query | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Pagination cursor |
limit | number | 20 | Page 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>
| Query | Type | Default | Description |
|---|---|---|---|
cursor | string | — | Pagination cursor |
limit | number | 30 | Page 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
| Status | Reason |
|---|---|
401 | Missing / invalid bearer token |
403 | Not a participant of the chat |
404 | Chat 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.