Skip to main content

Chat API

Planned

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

MethodPathDescription
GET/chatsList my chats
GET/chats/:idGet a chat
POST/chatsCreate a direct chat (others are auto-created)
DELETE/chats/:idSoft-delete (per-user — leaves the chat)
GET/chats/:id/messagesPaginate messages
POST/chats/:id/messagesSend a message
PATCH/chats/:id/messages/:msgIdEdit a message (sender, ≤15 min)
DELETE/chats/:id/messages/:msgIdSoft-delete a message

Chat types

TypeCreated by
directPOST /chats
event_authorsAuto-created when event publishes
approved_groupAuto-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();
ChannelSourceEvents
chat:{chatId}postgres_changes on messages filtered by chat_idINSERT / 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

StatusReason
403Not a participant of the chat
403Tried to edit someone else's message, or edit window expired
404Chat / 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_id is composite-indexed with created_at DESC for fast paginated reads.
  • Module layout will follow the shipped pattern: src/feats/chats/{router,service,schemas,constants}.ts.