6.1 KiB
Sending & receiving media in chat
Backend → frontend handoff. How image, video, and voice messages move through MinIO and the /messages/ endpoint. One decision is still open and blocks upload — see §5.
| Service | gooyal_chat backend |
| Endpoint base | /api/chats/<chat_uuid>/messages/ |
| Status | text messages ready · media upload path blocked |
1. How it fits together
The backend never touches file bytes. Media is written straight from the client to MinIO; the chat backend only stores a short text reference to it (relayed through Mattermost) and, on read, signs a temporary download link. Two separate hops, two separate systems:
sequenceDiagram
participant C as Client
participant M as MinIO
participant B as Chat backend
participant MM as Mattermost
Note over C,M: Upload phase
C->>M: PUT file bytes to object_key
M-->>C: 200 OK
Note over C,B: Send phase
C->>B: POST /messages/ { message_type, object_key }
B->>MM: post "image:object_key" as text
MM-->>B: post_id
B-->>C: 201 { post_id, url }
Note over C,B: Read phase
C->>B: GET /messages/
B->>MM: fetch posts
B->>M: presign GET for each object_key
M-->>B: signed URL (60 min TTL)
B-->>C: 200 [{ post_id, url, ... }]
The consequence: the backend cannot validate a file it never receives, and it never caches a download URL — every read re-signs one fresh.
2. Sending a media message
- Client — pick a unique object key and
PUTthe file to MinIO directly (bucketchat). Suggested key shape:<type>s/<conversation_uuid>/<random-id>-<filename>— the backend imposes no format, but a conversation-scoped, collision-proof key keeps the bucket sane. - Client — enforce type and size limits before upload. The backend does not check either — see §6.
- Client — call
POST /messages/with theobject_keythat was just uploaded, and the matchingmessage_type. - Backend — stores the message in Mattermost as text
"<type>:<object_key>"and returns it with a freshly-signedurlfor immediate display.
Request
POST /api/chats/{chat_uuid}/messages/
{
"sender_uuid": "9f2c1e2a-...",
"message_type": "image",
"object_key": "images/4b1c.../8a21-sunset.jpg"
}
| Field | Type | Rule |
|---|---|---|
sender_uuid |
UUID | required |
message_type |
enum (text | image | video | voice) |
optional — defaults to text |
text |
string | required only when message_type is text |
object_key |
string | required when message_type is image/video/voice — just checked for non-empty, nothing else |
Response — 201
{
"post_id": "prt_9f81...",
"sender_uuid": "9f2c1e2a-...",
"message_type": "image",
"mattermost_message_type": "",
"text": null,
"url": "https://minio.internal/chat/images/...?X-Amz-Signature=...",
"created_at": "2026-07-26T09:14:02Z"
}
A 403 { "detail": "..." } comes back instead if the conversation has been closed.
3. Reading messages
GET /api/chats/{chat_uuid}/messages/?per_page=20&page=0
[
{
"post_id": "prt_9f81...",
"message_type": "voice",
"url": "https://minio.internal/chat/voice/...?X-Amz-Signature=...",
"created_at": "2026-07-26T09:12:40Z"
}
]
| Param | Default | Notes |
|---|---|---|
page |
0 |
ignored if since is present |
per_page |
20 |
— |
since |
— | Unix ms timestamp; use for polling new messages instead of paging |
Don't cache the
urlfield. It's signed for 60 minutes (MINIO_PRESIGN_EXPIRY_SECONDS) and re-generated on every request — it is never stored. If a link goes stale (e.g. a long-lived chat view), re-fetch the message rather than reusing the old URL.
4. Object key convention
The backend treats object_key as an opaque string — it validates nothing beyond "non-empty" and constructs no path itself. That freedom is also the risk: two clients that pick colliding keys silently overwrite each other's file in MinIO with no error from this API. Recommended shape:
images/{conversation_uuid}/{uuid4}-{original_filename}
video/{conversation_uuid}/{uuid4}-{original_filename}
voice/{conversation_uuid}/{uuid4}-{original_filename}
Bucket is chat (MINIO_BUCKET_CHAT) in every environment unless ops overrides it.
5. Open question — how does the client get write access to MinIO?
⚠ Blocks implementation. This backend exposes no upload endpoint — no presigned-PUT route, no STS token issuance, nothing.
StorageServiceonly signs download URLs. There is currently no documented way for a client to legitimately write to thechatMinIO bucket.
Before building the upload step, confirm with backend/infra which of these it will be:
- A new
POST /messages/upload-url/-style endpoint that returns a short-lived presigned PUT URL for a given key (mirrors the existing download flow) — most likely path, but doesn't exist yet. - Direct client-held MinIO credentials scoped to the bucket (no backend round-trip).
- An anonymous/public-write bucket policy scoped by key prefix.
Don't build against an assumption here — the upload half of this feature can't ship until backend confirms one of the above (or another option) and, if it's the first, implements the endpoint.
6. What the backend won't check for you
- File type / size / content. The only server-side check is "
object_keyis non-empty." Enforce accepted mime types and max size client-side before upload. - That the object actually exists. Nothing verifies the key was really written to MinIO before the message is sent. A typo'd key sends successfully and just renders as a broken link later.
- Auth on the messages endpoint.
MessageViewcurrently has no authentication or permission check at all — anyone who can reach the route can post as anysender_uuid. Don't build client trust assumptions (e.g. "the server verified this sender") on top of it; treat it as likely to change.
gooyal_chat · backend↔frontend handoff · generated 2026-07-26