Build a chat inbox
This guide walks you through the steps to build a chat inbox using the XMTP SDK.
Pick your SDK
Web
Mobile
Create or build a client
Create an account signer
This code defines two functions that convert different types of Ethereum accounts—Externally Owned Accounts (EOAs) and Smart Contract Wallets (SCWs)—into a unified Signer
interface. This ensures that both account types conform to a common interface for message signing and deriving shared secrets as per MLS (Message Layer Security) requirements.
jhaaaa these EOA and SCW code samples include "address" in the comments to callout when the kind is Ethereum and the identifier is an Ethereum address. Is this OK or have I misunderstood?
-
For an EOA, the
convertEOAToSigner
function creates a signer that can get the account identity and sign messages and has placeholder methods for chain ID and block number.Browserimport type { Signer } from "@xmtp/browser-sdk"; const accountIdentity = { kind: "ETHEREUM", // Specifies the identity type identifier: "0x...", // Ethereum address as the identifier }; const signer: Signer = { getIdentity: () => accountIdentity, signMessage: async (message) => { // return value from a signing method here }, };
-
For an SCW, the
convertSCWToSigner
function similarly creates a signer but includes a specific implementation for chain ID and an optional block number computation.Browserimport type { Signer } from "@xmtp/browser-sdk"; const accountIdentity = { kind: "ETHEREUM", // Specifies the identity type identifier: "0x...", // Smart Contract Wallet address }; const signer: Signer = { getIdentity: () => accountIdentity, signMessage: async (message) => { // return value from a signing method here }, // These methods are required for smart contract wallets getBlockNumber: () => undefined, // Optional block number getChainId: () => BigInt(8453), // Example: Base chain ID };
Create an XMTP client
Create an XMTP MLS client that can use the signing capabilities provided by the SigningKey
parameter. This SigningKey
links the client to the appropriate EOA or SCW.
🎥 walkthrough: Create a client
This video provides a walkthrough of creating a client, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details.
Create a client
import { Client, type Signer } from "@xmtp/browser-sdk";
const accountIdentity = {
kind: "ETHEREUM", // Specifies the identity type
identifier: "0x...", // Ethereum address as the identifier
};
const signer: Signer = {
getIdentity: () => accountIdentity,
signMessage: async (message) => {
// return value from a signing method here
},
};
// This value should be generated once per installation and stored securely
const encryptionKey = window.crypto.getRandomValues(new Uint8Array(32));
const client = await Client.create(
signer,
encryptionKey,
options /* optional */
);
When an app first calls Client.create()
, a client creates a local database to manage messaging between the app and the network. In subsequent calls, it loads the existing database. The database is encrypted using the keys from the Signer
 interface.
To learn more about database operations, see the XMTP MLS protocol spec.
Configure an XMTP client
You can configure an XMTP client with these parameters of Client.create
:
jhaaaa how about keystoreProviders, persistConversations, skipContactPublishing, codecs, maxContentSize, preCreateIdentityCallback, preEnableIdentityCallback, basePersistence, apiClientFactory
Parameter | Default | Description |
---|---|---|
env | DEV | Connect to the specified XMTP network environment. Valid values include DEV , PRODUCTION , or LOCAL . For important details about working with these environments, see XMTP DEV, PRODUCTION, and LOCAL network environments. |
apiURL | undefined | Manually specify an API URL to use. If specified, value of env will be ignored. |
appContext (Android-only) | null | Required. The app context used to create and access the local database. |
dbEncryptionKey | null | Required. A 32-byte ByteArray used to encrypt the local database. |
dbDirectory | xmtp_db | Optional. Specify a database directory. If no directory is specified, the value is set to xmtp_db by default. |
historySyncUrl | https://message-history.dev.ephemera.network/ | The history sync URL used to specify where history can be synced from other devices on the network. For production apps, use https://message-history.production.ephemera.network . |
XMTP DEV, PRODUCTION, and LOCAL network environments
XMTP provides DEV
, PRODUCTION
, and LOCAL
network environments to support the development phases of your project.
The PRODUCTION
and DEV
networks are completely separate and not interchangeable.
For example, an XMTP identity on the DEV
network is completely distinct from the XMTP identity on the PRODUCTION
network, as are the messages associated with these identities. In addition, XMTP identities and messages created on the DEV
network can't be accessed from or moved to the PRODUCTION
network, and vice versa.
Here are some best practices for when to use each environment:
-
DEV
: Use to have a client communicate with theDEV
network. As a best practice, setenv
toDEV
while developing and testing your app. Follow this best practice to isolate test messages toDEV
inboxes. -
PRODUCTION
: Use to have a client communicate with thePRODUCTION
network. As a best practice, setenv
toPRODUCTION
when your app is serving real users. Follow this best practice to isolate messages between real-world users toPRODUCTION
inboxes. -
LOCAL
: Use to have a client communicate with an XMTP node you are running locally. For example, an XMTP node developer can setenv
toLOCAL
to generate client traffic to test a node running locally.
The PRODUCTION
network is configured to store messages indefinitely. XMTP may occasionally delete messages and keys from the DEV
network, and will provide advance notice in the XMTP Community Forms.
Build an existing client
Build, or resume, an existing client that's logged in and has an existing local database.
Client.build(identity, {
env: "production", // 'local' | 'dev' | 'production'
dbEncryptionKey: keyBytes, // 32 bytes
});
Log out a client
When you log a user out of your app, you can give them the option to delete their local database.
await client.deleteLocalDatabase()
await Client.dropClient(client.installationId)
Check if an identity is reachable
The first step to creating a conversation is to verify that participants’ identities are reachable on XMTP. The canMessage
method checks each identity's compatibility, returning a response indicating whether each identity can receive messages.
Once you have the verified identities, you can create a new conversation, whether it's a group chat or direct message (DM).
import { Client } from "@xmtp/browser-sdk";
// response is a Map of string (identity) => boolean (is reachable)
const response = await Client.canMessage([bo.identity, caro.identity]);
Create a conversation
Create a new group chat
Once you have the verified identities, create a new group chat:
const group = await client.conversations.newGroup(
[bo.identity, caro.identity],
createGroupOptions /* optional */
);
Create a new DM
Once you have the verified identity, get its inbox ID and create a new DM:
const group = await client.conversations.newDm(bo.inboxId);
List conversations and messages
List new group chats or DMs
Get any new group chats or DMs from the network:
await client.conversations.sync();
Sync
Sync all new messages and conversations from the network.
🎥 walkthrough: Syncing
This video provides a walkthrough of syncing, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details.
Sync
await client.conversations.syncAll();
Handle unsupported content types
As more custom and standards-track content types are introduced into the XMTP ecosystem, your app may encounter content types it does not support. This situation, if not handled properly, could lead to app crashes.
Each message is accompanied by a fallback
property, which offers a descriptive string representing the content type's expected value. It's important to note that fallbacks are immutable and are predefined in the content type specification. In instances where fallback
is undefined
, such as read receipts, it indicates that the content is not intended to be rendered. If you're venturing into creating custom content types, you're provided with the flexibility to specify a custom fallback string. For a deeper dive into this, see Build custom content types.
const codec = client.codecFor(content.contentType);
if (!codec) {
/*Not supported content type*/
if (message.fallback !== undefined) {
return message.fallback;
}
// Handle other types like ReadReceipts which are not meant to be displayed
}
List existing group chats or DMs
Get a list of existing group chats or DMs in the local database. By default, the conversations are listed in descending order by their lastMessage
created at value. If a conversation does not contain any messages, the conversation is ordered by its createdAt
value.
const allConversations = await client.conversations.list();
const allGroups = await client.conversations.listGroups();
const allDms = await client.conversations.listDms();
Stream conversations and messages
Stream all group chats and DMs
Listens to the network for new group chats and DMs. Whenever a new conversation starts, it triggers the provided callback function with a ConversationContainer
object. This allows the client to immediately respond to any new group chats or DMs initiated by other users.
const stream = await client.conversations.stream();
// to stream only groups, use `client.conversations.streamGroups()`
// to stream only dms, use `client.conversations.streamDms()`
try {
for await (const conversation of stream) {
// Received a conversation
}
} catch (error) {
// log any stream errors
console.error(error);
}
Stream all group chat and DM messages
Listens to the network for new messages within all active group chats and DMs. Whenever a new message is sent to any of these conversations, the callback is triggered with a DecodedMessage
object. This keeps the inbox up to date by streaming in messages as they arrive.
// stream all messages from all conversations
const stream = await client.conversations.streamAllMessages();
// stream only group messages
const stream = await client.conversations.streamAllGroupMessages();
// stream only dm messages
const stream = await client.conversations.streamAllDmMessages();
try {
for await (const message of stream) {
// Received a message
}
} catch (error) {
// log any stream errors
console.error(error);
}
Helper methods and class interfaces
Conversation helper methods
Use these helper methods to quickly locate and access specific conversations—whether by conversation ID, topic, group ID, or DM identity—returning the appropriate ConversationContainer, group, or DM object.
jhaaaa ConvoId should be renamed to conversationId - jha: docs did not include any instances of convoId
// get a conversation by its ID
const conversationById = await client.conversations.getConversationById(
conversationId
);
// get a message by its ID
const messageById = await client.conversations.getMessageById(messageId);
// get a 1:1 conversation by a peer's inbox ID
const dmByInboxId = await client.conversations.getDmByInboxId(peerInboxId);
Conversation union type
Serves as a unified structure for managing both group chats and DMs. It provides a consistent set of properties and methods to seamlessly handle various conversation types.
- React Native: Conversation.ts
Group class
Represents a group chat conversation, providing methods to manage group-specific functionalities such as sending messages, synchronizing state, and handling group membership.
- React Native: Group.ts
Dm class
Represents a DM conversation, providing methods to manage one-on-one communications, such as sending messages, synchronizing state, and handling message streams.
- React Native: Dm.ts