Chat
MAGMA ships an in-app chat system: community rooms and per-market rooms, served under
/v1/chat. Unlike most of the protocol's newer surfaces, chat is live and wired — the routes are
registered unconditionally (no feature flag) and backed by a live Postgres table.
Rooms are addressed by a roomId string
A room is identified by a prefixed string, which determines what the room is about:
roomId shape | Room |
|---|---|
market:<marketId> | The chat attached to a specific market's detail page |
community:<name> | A shared community lounge (e.g. community:general, community:traders) |
narrative:<id> | The chat attached to a specific narrative |
global | The shared global feed |
One reusable chat panel drives all of them. Note that the storage layer's room_type column only
distinguishes global vs narrative; market: and community: rooms are distinguished by the
room_id string, not by a separate stored type.
Identity is server-verified, never client-supplied
This is the load-bearing security property. The author of every message is derived from the server-verified auth token — never from the request body.
- Requests authenticate with
requireAnyAuth, which accepts a Privy JWT (web) or a MAGMA JWT (mobile). The MAGMA JWT carries the cryptographically-bound wallet, issued only after an ed25519 wallet-signature challenge (POST /v1/auth/verify). - Any
walletfield in the request body is ignored — the server writes the message under the wallet bound to the session. A client therefore cannot post as another wallet. - A bare Privy token with no wallet claim is rejected
401 wallet_session_requireduntil it is upgraded viaPOST /v1/auth/verify.
Clients follow the same contract: they never send a wallet field and pass only the Bearer
access token.
This endpoint previously trusted body.wallet, which let anyone post as any wallet. Do not
reintroduce a client-supplied author — the only accepted identity is the one bound to the verified
session token.
The surface
| Operation | Endpoint | Notes |
|---|---|---|
| Post a message | POST under /v1/chat/:roomId | Author is server-derived; body.wallet ignored. Enforces the rate limit and length cap below |
| Read history | GET under /v1/chat/:roomId | Returns non-deleted messages; each enriched with the author's Conviction tier badge |
| Live stream | GET /v1/chat/:roomId/stream | Server-Sent Events (text/event-stream), 30s keepalive ping |
| Delete a message | DELETE /v1/chat/:roomId/message/:id | Admin only — gated by the x-admin-secret header; soft-delete (sets deleted_at) |
Refer to the live API Reference for exact request/response schemas.
Realtime transport — SSE, not WebSockets
Rooms stream over Server-Sent Events: a client opens an EventSource against
/v1/chat/:roomId/stream, and the server broadcasts new messages to that room via an in-memory
per-room client map (with a 30-second keepalive). Clients keep a slow (~10s) polling fallback for
environments where a proxy strips SSE, plus optimistic send with message de-duplication on merge.
Moderation, rate limits, and storage
- Soft-delete moderation.
DELETE(admin,x-admin-secret) setsdeleted_at; every read filtersdeleted_at IS NULL, so a removed message disappears for all readers and never returns in history. - Rate limit. One message per wallet every few seconds (roughly 1 / 3s); a breach returns
429. - Length cap. Messages are capped at 1,000 characters, enforced at the API, in the database
CHECKconstraint, and client-side. - Storage. Messages live in a Postgres (Supabase) table
chat_messages— columnsid,room_id,room_type,narrative_id,wallet_address,message,reply_to_id,created_at,deleted_at.wallet_addressis always server-derived from the verified token.reply_to_idsupports threaded replies.
Not a resolution surface
Chat is a social surface only — nothing posted in a room affects how a market resolves or settles. Resolution flows exclusively through the oracle committee and the resolution engine.