3.2 KiB
| read_when | |||
|---|---|---|---|
|
Messages
Channel messages are the core durable object. Every message is Markdown text
with optional attachments. Threads are modelled as messages with a non-null
parent_message_id (see threads.md). Inline quote-replies —
the lightweight Discord/Telegram-style "reply with quote" pattern — live on
the same row via quoted_message_id and friends, documented in
replies.md.
Endpoints
GET /api/channels/{channel_id}/messages?after_seq=&limit=
POST /api/channels/{channel_id}/messages
PATCH /api/messages/{message_id}
DELETE /api/messages/{message_id}
GETreturns root messages only (parent_message_id IS NULL) for the channel, ordered bychannel_seqascending.after_seqis exclusive;limitis clamped to1..200(default 100).POSTaccepts{body}. Empty bodies are rejected.PATCHaccepts{body}and only the original author can edit. Setsedited_at.DELETEis a soft delete — setsdeleted_at, keeps the row and thechannel_seqslot so cursors stay valid.
All four emit durable events: message.created, message.updated,
message.deleted.
Sequence numbers
Every channel message gets a per-channel channel_seq assigned inside the
insert transaction:
SELECT COALESCE(MAX(channel_seq), 0) + 1
FROM messages
WHERE channel_id = ? AND parent_message_id IS NULL
That sequence is what clients page by, what the realtime event carries, and
what reconnect uses to backfill. It is monotonic per channel but not globally.
Thread replies use a separate thread_seq instead.
Body format
Bodies are stored as Markdown text. The body_format column is hard-coded to
markdown in V1 and exists so a future format (rich text, plain) can be added
without a migration. The frontend renders a sanitized subset.
The web composer is a Slack-like message well with a format bar for bold, italic, inline code, code blocks, links, attachments, and GIF insertion. The GIF picker inserts standard Markdown image syntax, so no provider-specific durable schema is required for V1.
Attachments
Messages carry zero or more attachments via the message_attachments join
table. Hydration happens in hydrateAttachments and surfaces as the
attachments field on Message. See uploads.md for the
two-step upload-then-attach flow.
The web client renders image and video attachments inline and links other attachments as file cards. Clicking an inline image attachment, or an image inside rendered Markdown, opens an in-app image viewer with an Open original link. Markdown image URLs, including animated GIF URLs, render inline through the same sanitized Markdown path.
Author hydration
ListMessages and GetThread join users and populate Message.author so
clients don't need a second round-trip. Avatar URLs are passed through as-is.
What is intentionally missing
- Hard delete. The soft-delete row stays for cursor stability.
- Pinning, bookmarks, read receipts.
- Per-message permissions beyond "the author can edit/delete".