Documentation

Payload Formats

Encode text or JSON, validate received data, and adapt a serializer without changing the SDK.

Before you start: obtain a connected chat segment using Messaging. Each format below is an alternative: both sender and receiver must agree on the format.

1. Start with text

Use these alternatives in web/app.js, replacing the existing message listener and payload creation. Keep the Quickstart's error handling 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 incoming data. Validate anything received from a publisher you do not control. Render received text safely rather than inserting 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 object properties containing undefined are omitted. Validate application data before encoding. The reading 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 app and protobuf.js. It reads a small schema directly, so no code-generation step is needed.

  1. Install the serializer in your demo project:
npm install protobufjs
  1. Create web/reading.proto. Both tabs must use this schema:
web/reading.proto
syntax = "proto3";

package demo;

message Reading {
  string sensor = 1;
  double value = 2;
}

The numbers identify fields on the wire; keep them stable when changing your schema.

  1. Create web/reading-codec.js:
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; finish() produces the bytes Celeris expects. verify() checks field types, while validateReading() also checks this application's rules.

  1. In web/app.js, add the import and replace the existing text-message listener with this listener before connecting:
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 existing chat.subscribe() and cleanup call to stopMessages(). Do not register the old text or MessagePack listener as well.

  1. In web/index.html, change the input label from Message to Sensor, keeping its existing ID. In web/app.js, replace the text publish in the form's existing try block 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 existing Sent locally log with log("Sent a reading locally."); so it describes the new payload.

Reload both tabs and connect them before sending. Enter temperature in the Sensor field and press Send. Expected: the other tab's Activity list shows temperature: 21.5. The sender sees no received copy unless echo is enabled. Publish completion still means local acceptance, not confirmed delivery.

Do not mix text, JSON, MessagePack, and Protobuf receivers on the same segment unless your application defines how to distinguish formats. Separate segments or an explicit envelope can provide that distinction. Successfully decoding Protobuf does not prove who sent it 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:

ValueJavaScript type
Presence request page and perPagenumber
Presence result total, perPage, currentPage, from, and tonumber
Connection and presence-query timeouts in millisecondsnumber
Credential request disconnectedAt and replayLookbackMsnumber when present
Recovery event retryIndexnumber
Message, notice, presence-event, and presence-record timestampbigint

Use normal number arithmetic for counts and pages, such as page + 1. Keep timestamps as bigint when retaining their exact value. 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, leaving 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 following the same contract can use BigInt(record.timestamp) to restore it. Validate incoming records before conversion.

For nested SDK data, such as a presence page, use a replacer. Numeric counts remain JSON numbers; 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.

Expected: text arrives as a string, valid JSON passes the schema, invalid JSON is ignored, and the custom codec returns the object you encoded.

Next: Presence. For format tradeoffs, see Binary Messaging.

We use Google Analytics cookies to understand how people use Celeris, only if you allow it. See our Cookie Policy.