Chat API
| Status | Implementation |
|---|---|
| ✅ Shipped (read-only) | 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 Sending
Message sending is done via Supabase Realtime, not through this REST API. The API provides read-only access to chat history.
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 | 🚧 Planned | Create a direct chat |
| POST | /chats/:id/messages | — | Use Supabase Realtime |
| PATCH | /chats/:id/messages/:msgId | 🚧 Planned | Edit a message |
| DELETE | /chats/:id/messages/:msgId | 🚧 Planned | Soft-delete a message |
Chat types
| Type | Created by | Status |
|---|---|---|
approved_group | Auto-created on group approval | ✅ Shipped |
direct | POST /chats (planned) | 🚧 Planned |
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).
{
"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 sending and real-time updates use Supabase Realtime channels.
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.