WebSocket gives you a stream of opaque messages. Adding structure is your job. The dominant pattern across the industry — Slack, Discord, GraphQL-WS, every chat library — is a small JSON envelope with a type string and a data object. The type field tells you which schema the data follows. Everything else falls out from this single decision.
Namespacing types
Use dot-notation: chat.message, chat.typing, user.joined, user.status, room.join, game.move. The first segment is the domain; the second is the verb or event. Stays sortable, greppable, and natural to extend.
Errors are messages too
An error response is just another message: {type: 'error', code: 'rate_limited', message: '...', ref_id: 'xyz'}. Same envelope, different schema. Treat errors as first-class, not as a special transport-level concept.
Define the protocol for a simple chat app: list every message type with fields (chat.message, chat.typing, user.joined, user.left, room.join, room.leave, error). Write it as an AsyncAPI 3.0 document. Even if you don't generate code from it, the act of writing the spec catches missing fields.
Progress
Progress is local-only — sign in to sync across devices.