Schemas and Versioning
A real WebSocket app is never deployed once. The server ships fixes; clients update on their own schedule; an old mobile build keeps talking to a new server for months. Your message protocol has to survive that churn — and the two ideas that make it survivable are a message envelope and a version field.
The envelope
Section titled “The envelope”Rather than letting every message be an arbitrary shape, agree on a consistent outer structure — an envelope — and put the message-specific data inside it:
type— what kind of message this is (the dispatch key from the framing lesson).v— the protocol/schema version this message conforms to.payload— the actual data, the part that varies bytype.
Every message, in both directions, looks the same on the outside. Routing code reads type, compatibility code reads v, and only the per-type handlers ever touch payload. A well-formed message reads like {"type":"chat","v":1,"payload":{...}}.
flowchart LR
subgraph env["Envelope — same for every message"]
t["type: what"]
v["v: which version"]
p["payload: the varying data"]
end
t --> route["dispatch / routing"]
v --> compat["compatibility handling"]
p --> handler["per-type handler"] Validate what arrives — never trust the wire
Section titled “Validate what arrives — never trust the wire”Anything can arrive on a socket: a buggy client, an old build, a malformed or even malicious message. Before a handler touches a message, validate the envelope: it must parse as JSON, have a known type, carry a v, and contain the fields that type requires. Reject anything else with a clear error message rather than letting it crash a handler half-way through. In production you would do this with a schema library (Zod, Ajv/JSON Schema, or generated Protobuf validators); the principle is identical at any scale: incoming data is untrusted until proven well-formed.
Evolve additively
Section titled “Evolve additively”Once clients are in the wild, you cannot change the meaning of an existing field — an old client still reads it the old way. The rule that keeps a protocol alive:
- Add, never repurpose. New optional fields are safe; old clients ignore what they do not recognize, new clients read the extra data. Removing a field or changing its meaning breaks someone.
- Bump
vfor breaking changes. When a change genuinely cannot be additive, raise the version and have the server support bothvvalues during the migration window, branching onv. - Default the missing. When a new field is absent (an older client did not send it), the receiver supplies a sensible default instead of failing.
These three habits let a v1 and a v2 client share the same server indefinitely.
A demo: validate, then evolve
Section titled “A demo: validate, then evolve”The demo validates every incoming envelope, then shows additive evolution in action: the server understands a v1 chat message and a v2 message that adds an optional lang field — handling both, and defaulting lang when an older client omits it. A deliberately malformed message is rejected, not crashed on. It runs on an in-page echo socket using the real WebSocket API.
The v1 message and the v2 message both flowed through the same handleChat, the new lang field appearing only where it was sent and defaulting to en where it was not — that is additive evolution working. The third message never reached a handler because validation stopped it at the door. Build those two habits in from day one and your protocol can grow for years without a flag day.