Payload Formats
Encode text or JSON, validate received data, and adapt a serializer with no change to the SDK.
Before you start: obtain a connected chat segment with the setup from Messaging. Each format below is an alternative. Both the sender and the receiver must agree on the format.
1. Start with text
Use these alternatives in web/app.js. Replace the current message listener and payload creation. Keep the Quickstart's error handlers around publishes.
import { textPayload, readText } from "@useceleris/client";
const stopText = chat.onMessage((payload) => console.log(readText(payload)));
await chat.publish({ payload: textPayload("Hello") });
// Later, when this receiver is no longer needed:
stopText();
Celeris carries opaque bytes. It does not infer a content type, compress JSON automatically, or translate formats for recipients.
2. Send and validate JSON
Install a validator explicitly in your application:
npm install zod
import { jsonPayload, readJson } from "@useceleris/client";
import { z } from "zod";
const typingSchema = z.object({
type: z.literal("typing"),
active: z.boolean(),
});
const stopJson = chat.onMessage((payload) => {
try {
const result = typingSchema.safeParse(readJson(payload));
if (!result.success) {
console.warn("Ignored an invalid typing event.");
return;
}
console.log("Peer typing:", result.data.active);
} catch {
console.warn("Ignored a payload that was not valid JSON.");
}
});
await chat.publish({
payload: jsonPayload({ type: "typing", active: true }),
});
// Later:
stopJson();
In TypeScript, readJson<MyType>() asserts a type. It does not validate received data. Validate anything received from a publisher that you do not control. Render received text safely. Do not insert it as HTML.
jsonPayload() uses JavaScript's JSON.stringify(). It throws ConfigurationError if serialization throws, such as for a circular object or a bigint, or produces no JSON at all. It is not schema validation. Values such as NaN become null, and it omits object properties that contain undefined. Validate application data before you encode it. The read helpers reject invalid UTF-8 or malformed JSON.
3. Bring your own serializer
The SDK includes one adapter, not a separate bundled library for every format. Your encoder must return a Uint8Array. Errors from your encoder or decoder propagate unchanged.
MessagePack
Install your chosen serializer:
npm install @msgpack/msgpack
import { createPayloadCodec } from "@useceleris/client";
import { encode, decode } from "@msgpack/msgpack";
const codec = createPayloadCodec({ encode, decode });
const stopBinary = chat.onMessage((payload) => {
const value = codec.readPayload(payload);
console.log(value); // Validate the decoded value before trusting it.
});
await chat.publish({
payload: codec.encodePayload({ sensor: "temperature", value: 21.5 }),
});
// Later:
stopBinary();
Protobuf
This example uses the Quickstart's Vite browser application and protobuf.js. It reads a small schema directly, so you do not need a code-generation step.
- Install the serializer in your demo project:
npm install protobufjs
- Create
web/reading.proto. Both tabs must use this schema:
syntax = "proto3";
package demo;
message Reading {
string sensor = 1;
double value = 2;
}
The numbers identify fields on the wire. Keep them stable when you change your schema.
- Create
web/reading-codec.js:
import protobuf from "protobufjs";
import { createPayloadCodec } from "@useceleris/client";
import schemaSource from "./reading.proto?raw";
const root = protobuf.parse(schemaSource).root;
const Reading = root.lookupType("demo.Reading");
function validateReading(reading) {
const error = Reading.verify(reading);
if (error) throw new Error("Invalid reading: " + error);
if (!reading.sensor?.trim() || !Number.isFinite(reading.value)) {
throw new Error("A reading needs a sensor name and a finite value.");
}
}
export const protoCodec = createPayloadCodec({
encode(reading) {
validateReading(reading);
return Reading.encode(reading).finish();
},
decode(bytes) {
const reading = Reading.decode(bytes);
validateReading(reading);
return reading;
},
});
Vite's ?raw import loads the schema as a string. encode() returns a writer, and finish() produces the bytes that Celeris expects. verify() checks field types. validateReading() also checks this application's rules.
- In
web/app.js, add the import and replace the current text-message listener with this listener before you connect:
import { protoCodec } from "./reading-codec.js";
const stopMessages = chat.onMessage((payload) => {
try {
const reading = protoCodec.readPayload(payload);
log(reading.sensor + ": " + reading.value);
} catch {
log("Ignored an invalid Protobuf reading.");
}
});
Keep the current chat.subscribe() and cleanup call to stopMessages(). Do not register the old text or MessagePack listener as well.
- In
web/index.html, change the input label fromMessagetoSensor. Keep its current ID. Inweb/app.js, find the text publish in the form's currenttryblock. Replace it with a reading that uses that input and a sample value:
await chat.publish({
payload: protoCodec.encodePayload({
sensor: messageInput.value.trim(),
value: 21.5,
}),
});
Also replace the current Sent locally log with log("Sent a reading locally."); so it describes the new payload.
Reload both tabs and connect them before you send. Enter temperature in the Sensor field and press Send. The other tab's Activity list then shows temperature: 21.5. The sender sees no received copy unless your server enables echo. Publish completion still means local acceptance, not confirmed delivery.
Mix text, JSON, MessagePack, and Protobuf receivers on one segment only when your application can distinguish the formats. Separate segments or an explicit envelope can provide that distinction. A successful Protobuf decode does not prove who sent the message or which schema they used. Keep authorization and application validation in place.
4. Handle timestamps
The SDK's metadata and your message payload are separate. Your serializer decides the types inside a payload. The SDK uses these types for its own numeric fields:
| Value | JavaScript type |
|---|---|
Presence request page and perPage | number |
Presence result total, perPage, currentPage, from, and to | number |
| Connection and presence-query timeouts in milliseconds | number |
Credential request disconnectedAt and replayLookbackMs | number when present |
Recovery event retryIndex | number |
Message, notice, presence-event, and presence-record timestamp | bigint |
Use normal number arithmetic for counts and pages, such as page + 1. Keep timestamps as bigint when their exact value matters. Do not convert them blindly with Number(). A 64-bit integer can exceed JavaScript's safe-integer range. Arithmetic cannot mix a bigint and a number.
In web/app.js, this listener prepares message metadata for a JSON API. It converts only the timestamp and leaves the SDK value unchanged:
const stopMetadata = chat.onMessage((_payload, metadata) => {
const serialized = JSON.stringify({
...metadata,
timestamp: metadata.timestamp.toString(),
});
console.log(serialized);
});
// Later:
stopMetadata();
The JSON timestamp is a decimal string, not a JSON number. A consumer that follows the same contract can use BigInt(record.timestamp) to restore it. Validate received records before conversion.
For nested SDK data, such as a presence page, use a replacer. Numeric counts remain JSON numbers, and nested timestamps become strings:
function stringifySdkData(value) {
return JSON.stringify(value, (_key, field) =>
typeof field === "bigint" ? field.toString() : field
);
}
// After connecting:
const result = await chat.presenceList({ page: 1, perPage: 25 });
const serialized = stringifySdkData(result);
ServerError.resource can also contain numbers, bigints, or nested arrays. The same replacer handles either numeric type. This is an application serialization convention, not a change to the SDK's runtime types. Do not log sensitive payloads, identities, or error resources in production.
After these steps, text arrives as a string, and valid JSON passes the schema. The listener ignores invalid JSON. The custom codec returns the object that you encoded.
Next: Presence. For format tradeoffs, see Binary Messaging.