Chat API
This module is not yet shipped. The shape below describes the intended endpoints on the new Hono + Supabase + Drizzle stack. Realtime fan-out uses Supabase Realtime (not a custom WebSocket server).
In-app messaging for event authors and approved groups, plus DMs.
See also: Chats model.
All HTTP endpoints require Authorization: Bearer <supabase-access-token>.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /chats | List my chats |
| GET | /chats/:id | Get a chat |
| POST | /chats | Create a direct chat (others are auto-created) |
| DELETE | /chats/:id | Soft-delete (per-user — leaves the chat) |
| GET | /chats/:id/messages | Paginate messages |
| POST | /chats/:id/messages | Send a message |
| PATCH | /chats/:id/messages/:msgId | Edit a message (sender, ≤15 min) |
| DELETE | /chats/:id/messages/:msgId | Soft-delete a message |
Chat types
| Type | Created by |
|---|---|
direct | POST /chats |
event_authors | Auto-created when event publishes |
approved_group | Auto-created on group approval |
List chats
GET /chats
{
"data": [
{
"id": "uuid",
"type": "approved_group",
"eventId": "uuid",
"name": "Saturday Night Party — group",
"participants": ["uuid", "..."],
"lastMessageAt": "2026-04-01T12:30:00Z",
"unreadCount": 3
}
],
"nextCursor": null
}
Create direct chat
POST /chats
{ "type": "direct", "participantIds": ["other-user-uuid"] }
Both users must be friends. If a direct chat between the same pair already exists, the existing one is returned (idempotent).
Messages
GET /chats/:id/messages?cursor=xxx&limit=50
POST /chats/:id/messages
{ "text": "Hello!", "attachmentUrl": "https://..." }
attachmentUrl should be a Supabase Storage signed URL produced by an
upload step (planned: POST /uploads returning a signed PUT URL).
PATCH /chats/:id/messages/:msgId
{ "text": "Updated text" }
- Only the sender may edit.
- Only within 15 minutes of
created_at. - Sets
edited_at.
DELETE /chats/:id/messages/:msgId
Soft delete (deleted_at). The row is retained but hidden from
non-moderator queries.
Realtime — Supabase Realtime
Realtime fan-out uses Supabase Realtime channels, not a custom
WebSocket server. 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();
| Channel | Source | Events |
|---|---|---|
chat:{chatId} | postgres_changes on messages filtered by chat_id | INSERT / UPDATE / DELETE |
chat:{chatId} | broadcast (server-emitted) | typing, presence (planned) |
Row-Level Security guarantees a user only receives messages for chats they are a participant of, even on the realtime stream.
Errors
| Status | Reason |
|---|---|
403 | Not a participant of the chat |
403 | Tried to edit someone else's message, or edit window expired |
404 | Chat / message not found |
Implementation notes
- Chat membership is enforced both in the API service layer and via RLS
policies on
chat_participants/messages. The realtime layer honours RLS automatically. messages.chat_idis composite-indexed withcreated_at DESCfor fast paginated reads.- Module layout will follow the shipped pattern:
src/feats/chats/{router,service,schemas,constants}.ts.