Skip to main content

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 shapeRoom
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
globalThe 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 wallet field 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_required until it is upgraded via POST /v1/auth/verify.

Clients follow the same contract: they never send a wallet field and pass only the Bearer access token.

Never trust a client-supplied author

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

OperationEndpointNotes
Post a messagePOST under /v1/chat/:roomIdAuthor is server-derived; body.wallet ignored. Enforces the rate limit and length cap below
Read historyGET under /v1/chat/:roomIdReturns non-deleted messages; each enriched with the author's Conviction tier badge
Live streamGET /v1/chat/:roomId/streamServer-Sent Events (text/event-stream), 30s keepalive ping
Delete a messageDELETE /v1/chat/:roomId/message/:idAdmin 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) sets deleted_at; every read filters deleted_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 CHECK constraint, and client-side.
  • Storage. Messages live in a Postgres (Supabase) table chat_messages — columns id, room_id, room_type, narrative_id, wallet_address, message, reply_to_id, created_at, deleted_at. wallet_address is always server-derived from the verified token. reply_to_id supports 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.