# Data conventions

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

Two conventions run through all the data in `stream-chat`: the server's timestamps are nanosecond numbers, and your own fields live under `custom`, typed by interfaces you extend.

## Timestamps

Every date the server sends is a `TimestampNS`: Unix time in nanoseconds, as a `number`. For example, a message sent on 9 October 2025 at 08:53:20 UTC has a `created_at` of `1760000000000000000`.

Compare and sort timestamps as they are, since they're plain numbers: `a.created_at < b.created_at`. Convert them where they meet a `Date`, a date library or `Date.now()`, with the helpers `stream-chat` exports:

| Helper                         | Returns                                                                                                          |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `nsToDate(ts)`                 | a `Date`                                                                                                         |
| `convertTimestampToDate(ts)`   | a `Date`, or `undefined` when `ts` is missing or not a finite number                                             |
| `nsToMs(ts)`                   | Unix time in milliseconds, for arithmetic with `Date.now()`                                                      |
| `nsToRfc3339(ts)`              | an RFC 3339 string that keeps the sub-millisecond part                                                           |
| `dateToNs(date)`, `msToNs(ms)` | a `TimestampNS` from a `Date` or from milliseconds                                                               |
| `nowNs()`                      | the current time as a `TimestampNS`                                                                              |
| `asTimestampNS(n)`             | the same number typed as a `TimestampNS`, for a value already in nanoseconds, such as one read back from storage |

```ts
import { nsToDate, nsToMs } from "stream-chat";

const sentAt = nsToDate(message.created_at);
const minutesAgo = Math.round(
  (Date.now() - nsToMs(message.created_at)) / 60_000,
);

console.log(`Sent ${sentAt.toLocaleString()}, ${minutesAgo} minutes ago`);
```

What goes wrong without them:

- `new Date(message.created_at)` reads nanoseconds as milliseconds and gives an invalid date. The types catch it: the result isn't typed as a `Date`, so using it as one doesn't compile.
- Passing the number to a date library, or comparing it with `Date.now()`, compiles but gives wrong results. Convert it first.
- An optional timestamp is missing when it isn't set. In the messages a message list holds, `pinned_at` and `deleted_at` are then `undefined`. Other optional timestamps, and every timestamp in a raw response or an event, can also be `null`. Test for a missing one with `== null`, which matches both; `=== null` misses `undefined`.

The fields of a request that take a date, such as a reminder's `remind_at`, are typed `Date`. To send a server timestamp back, pass `nsToDate(ts)`, or `nsToRfc3339(ts)` where the precision matters. Durations, delays and intervals in the client's settings are in milliseconds.

Nanosecond values are larger than JavaScript represents exactly, so they're rounded to about 256 nanoseconds. Ordering always holds, but two values may not be exactly equal after a round trip through JSON.

## Custom data types

Your own fields on a message, a channel, a user and the other objects live in that object's `custom` field: `message.custom`, `channel.data.custom`, `user.custom`. To type them, extend the matching interface of `stream-chat` in a declaration file that your TypeScript project includes:

```ts title="stream-custom-data.d.ts"
import "stream-chat";

declare module "stream-chat" {
  interface CustomMessageData {
    priority?: "high" | "normal";
  }

  interface CustomChannelData {
    image?: string;
    name?: string;
  }

  interface CustomUserData {
    department?: string;
  }

  interface CustomEventTypes {
    order_shipped: true;
  }

  interface CustomEventData {
    order_id?: string;
  }
}
```

Every read and write of those fields is then typed:

```ts
const priority = message.custom.priority; // "high" | "normal" | undefined
const channelName = channel.data?.custom?.name;

// A custom event: the type is declared in CustomEventTypes, its fields go under `custom`.
await channel.sendEvent({
  event: { type: "order_shipped", custom: { order_id: "42" } },
});

channel.on("order_shipped", (event) => {
  console.log(`Order ${event.custom.order_id} shipped`);
});
```

In the filters that take custom fields, a custom field takes a `custom.` prefix, as in `{ "custom.priority": "high" }`. These are the filters for channels, members, users, threads, polls, search and the moderation review queue. Other filters, such as those for reactions and reminders, don't take custom fields. A channel's `name` is the exception: filter it as `name`.

The interfaces, and what each one types:

| Interface                   | Types                                                                                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CustomMessageData`         | `message.custom`                                                                                                                                 |
| `CustomChannelData`         | `channel.data.custom`, and the `custom` you pass when creating a channel                                                                         |
| `CustomUserData`            | `user.custom`                                                                                                                                    |
| `CustomMemberData`          | a channel member's `custom`                                                                                                                      |
| `CustomAttachmentData`      | an attachment's `custom`. It already declares `mime_type` and `file_size`, and your fields add to them                                           |
| `CustomReactionData`        | a reaction's `custom`                                                                                                                            |
| `CustomPollData`            | a poll's `custom`                                                                                                                                |
| `CustomPollOptionData`      | a poll option's `custom`                                                                                                                         |
| `CustomThreadData`          | a thread's `custom`                                                                                                                              |
| `CustomEventData`           | an event's `custom`                                                                                                                              |
| `CustomEventTypes`          | your own event types: each key becomes a valid event type. Only the keys count, so any value type works                                          |
| `CustomMessageComposerData` | the data a composer keeps for the UI and doesn't send, see [Message composer](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-composer/) |

`CustomCommandData` is exported, but nothing reads it, so extending it has no effect.

A few things to keep in mind:

- A field you declare without `?` becomes required in every object of that type, including test data. Declare fields optional unless the server always sends them.
- Keep all the declarations in one file, so every part of the app sees the same types.
- Keep custom data small: it travels with every message, channel or user that carries it.
- After upgrading the SDK, check that the declarations still compile.

## Used in

- [SDK integration](https://getstream.io/chat/docs/sdk/react/v15/features/ai/sdk-integration/), for the custom fields of AI messages
- [Attachments and uploads](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/attachments-and-uploads/), for custom attachments

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/data-conventions/](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/data-conventions/).