Source document · API/docs/CHAT_MEDIA_NOTIFICATIONS.md
Chat, media and notifications
These systems share a common pattern: the Go API authenticates the caller and authorizes access to a specific conversation, asset, inbox, or device. PostgreSQL keeps durable metadata and state. WebSockets, object storage, email, and push services deliver data outside the API’s request/response path.
Chat conversations and messages
| Endpoint | What it does |
|---|---|
POST /api/v1/chat/conversations | Creates or reuses the one-to-one conversation for the caller and other_user_id. The two participants must be distinct. |
GET /api/v1/chat/conversations | Lists the signed-in user’s conversations (up to 50). It never lists another account’s conversations. |
GET /api/v1/chat/conversations/{id}/messages | Reads up to 50 messages after confirming that the caller is a participant. |
POST /api/v1/chat/conversations/{id}/messages | Sends a message as the caller. The path controls the conversation and the token controls the sender ID. |
POST /api/v1/chat/conversations/{id}/read | Marks the caller’s messages/read cursor in the conversation as read. |
GET /api/v1/chat/conversations/{id}/realtime | Upgrades an authenticated HTTP request to a WebSocket after checking conversation membership. |
The message body must be non-empty after trimming. The default message_type is text. The repository persists the message before notifying other participants; realtime delivery is a convenience layer, not the canonical history. A connected client should fetch messages after reconnecting and use the read endpoint to advance its read state.
Conversation creation is idempotent at the repository level: simultaneous requests from either participant converge on the same one-to-one conversation rather than creating duplicates. Message writes also support client_message_id in the persistence contract to make client retries safer; check the endpoint schema for which fields are currently described.
The WebSocket handshake uses the normal Authorization: Bearer <access-token> header. The server checks membership before upgrade, limits incoming frames to 1 KiB, sends keepalive pings every 25 seconds, and closes an idle connection after missed pong/read deadlines. The in-process hub is a fan-out path for clients connected to the same API process; horizontal multi-instance WebSocket delivery needs a shared pub/sub adapter before relying on messages crossing replicas. Persisted message history remains available from PostgreSQL regardless of the in-memory hub.
Upload and read media
POST /api/v1/media/upload does not upload file bytes through the API. It registers an asset and returns a short-lived signed object-storage PUT URL:
- Send
purpose,mime_type, andsize_byteswith a bearer token. - The API validates a positive size up to 10 MiB and MIME type
image/jpeg,image/png,image/webp, orvideo/mp4. - The API creates an asset ID, records its owner/purpose/size/type as private metadata, and asks the configured S3-compatible adapter for a 15-minute signed
PUTURL. - Upload bytes directly to that URL using the signed method and content type. The upload signature also includes the declared content length.
- Store the returned asset ID in a product record that supports media, such as a Moment or message attachment.
GET /api/v1/media/{id} checks the asset access policy and returns a signed GET URL valid for five minutes. Newly initialized assets are private to their owner. When an owned asset is attached to a public Moment, the community repository verifies the owner and moment purpose, associates the asset, and changes it to public visibility. Never expose raw storage keys or credentials. Do not assume every asset becomes public: message/document use cases can remain private.
The signed URL is a temporary capability. Keep it out of analytics and logs. A valid asset record does not guarantee an object was successfully uploaded; storage and client transfer errors must be handled separately. The local storage adapter deliberately returns unavailable, so real uploads require the configured HTTPS S3-compatible endpoint, bucket, access key, and secret.
In-app inbox and push notifications
| Endpoint | What it does |
|---|---|
POST /api/v1/devices | Registers the caller’s device token/platform; the user ID is taken from the bearer session. |
GET /api/v1/notifications | Lists up to 50 notifications for the caller. |
POST /api/v1/notifications/{id}/read | Acknowledges one notification belonging to the caller. |
Order creation records its notification event in the same PostgreSQL transaction as the order. The worker runs inside the API process, claims outbox rows with PostgreSQL locking, and publishes durable inbox entries. Delivery is at-least-once: if an event is retried after partial delivery, deterministic notification IDs prevent duplicate inbox rows. Temporary errors back off; after ten failed attempts an event is dead-lettered for operator review. A push provider failure does not roll back the order or delete the inbox record.
Firebase Cloud Messaging is optional. When both FIREBASE_PROJECT_ID and FIREBASE_CREDENTIALS_FILE are configured, persisted notifications can also be delivered to registered Android devices. Without them, the in-app inbox still works. Invalid device tokens should be removed when provider delivery identifies them as invalid.
Account email
Mail is separate from the user-facing notification inbox. The auth module uses email for account verification, sign-in OTPs, verification resend, and password reset. In production, MailAfrica is selected when MAILAFRICA_API_KEY is set; otherwise the SMTP adapter is used. OTPs are short-lived and single-use, stored hashed, and resend requests have a cooldown. See authentication and access control and production operations for lifecycle and secret configuration.
Operational boundaries
- Persisted messages, notifications, and media metadata are authoritative; WebSocket/push/signed URL delivery can fail independently.
- API process restarts drop active WebSocket hub subscriptions but do not delete PostgreSQL message history.
- Outbox workers retry delivery; they do not retry arbitrary client requests.
- FCM is optional and is not used to send account verification or sign-in codes.
- S3 object lifecycle and orphan cleanup need an operator policy; an expired metadata row or signed URL is not a substitute for bucket lifecycle management.