# 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](#5-open-question--how-does-the-client-get-write-access-to-minio). | | | |---|---| | **Service** | gooyal_chat backend | | **Endpoint base** | `/api/chats//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: ```mermaid 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 1. **Client** — pick a unique object key and `PUT` the file to MinIO directly (bucket `chat`). Suggested key shape: `s//-` — the backend imposes no format, but a conversation-scoped, collision-proof key keeps the bucket sane. 2. **Client** — enforce type and size limits *before* upload. The backend does not check either — see [§6](#6-what-the-backend-wont-check-for-you). 3. **Client** — call `POST /messages/` with the `object_key` that was just uploaded, and the matching `message_type`. 4. **Backend** — stores the message in Mattermost as text `":"` and returns it with a freshly-signed `url` for immediate display. ### Request `POST /api/chats/{chat_uuid}/messages/` ```json { "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` ```json { "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` ```json [ { "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 `url` field.** 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. `StorageService` only signs *download* URLs. There is currently no documented way for a client to legitimately write to the `chat` MinIO 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_key` is 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.** `MessageView` currently has no authentication or permission check at all — anyone who can reach the route can post as any `sender_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*