Skip to content

Build a quickstart chat app

Follow this tutorial to build a quickstart chat app with XMTP.

When you build your quickstart app, you will use a key pair to create an identity and register it with XMTP. This gives you an inbox ID, which is a stable identifier that serves as the destination for your messages.

Terminal window
npm install @xmtp/browser-sdk viem

The signer must return the account identifier and sign messages as bytes. backendUrl is required. Use the URL of your running backend. These examples create temporary identities for testing. Do not use their key lifecycle in a production app: store the signer key and database encryption key securely and reuse them with the same database on later launches.

The Browser and Node blocks come from compiled source files. They are checked against the SDK types during the docs build. The mobile examples create a second client so that the message recipient is registered on the same backend.

import {
class Client<ContentTypes = BuiltInContentTypes>

Client for interacting with the XMTP network

Client
,
enum ConsentState
ConsentState
,
enum IdentifierKind
IdentifierKind
,
type
type Signer = {
type: "EOA";
getIdentifier: GetIdentifier;
signMessage: SignMessage;
} | {
type: "SCW";
getIdentifier: GetIdentifier;
signMessage: SignMessage;
getBlockNumber?: GetBlockNumber;
getChainId: GetChainId;
}
Signer
,
} from "@xmtp/browser-sdk";
import {
function hexToBytes(hex_: Hex, opts?: HexToBytesOpts): ByteArray

Encodes a hex string into a byte array.

@paramhex Hex string to encode.

@paramopts Options.

@returnsByte array value.

@example import { hexToBytes } from 'viem' const data = hexToBytes('0x48656c6c6f20776f726c6421') // Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])

@example import { hexToBytes } from 'viem' const data = hexToBytes('0x48656c6c6f20776f726c6421', { size: 32 }) // Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

hexToBytes
} from "viem";
import {
function generatePrivateKey(): Hex

@description Generates a random private key.

@returnsA randomly generated private key.

generatePrivateKey
,
function privateKeyToAccount(privateKey: Hex, options?: PrivateKeyToAccountOptions): PrivateKeyAccount

@description Creates an Account from a private key.

@returnsA Private Key Account.

privateKeyToAccount
} from "viem/accounts";
// Use a new wallet and an in-memory database for this local test.
const
const account: {
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
signAuthorization: (parameters: AuthorizationRequest) => Promise<SignAuthorizationReturnType>;
signMessage: ({ message }: {
message: SignableMessage;
}) => Promise<Hex>;
signTransaction: <serializer extends SerializeTransactionFn<TransactionSerializable> = SerializeTransactionFn<TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: {
serializer?: serializer | undefined;
} | undefined) => Promise<Hex>;
signTypedData: <const typedData extends TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: TypedDataDefinition<typedData, primaryType>) => Promise<Hex>;
publicKey: Hex;
source: "privateKey";
type: "local";
}
account
=
function privateKeyToAccount(privateKey: Hex, options?: PrivateKeyToAccountOptions): PrivateKeyAccount

@description Creates an Account from a private key.

@returnsA Private Key Account.

privateKeyToAccount
(
function generatePrivateKey(): Hex

@description Generates a random private key.

@returnsA randomly generated private key.

generatePrivateKey
());
const
const signer: Signer
signer
:
type Signer = {
type: "EOA";
getIdentifier: GetIdentifier;
signMessage: SignMessage;
} | {
type: "SCW";
getIdentifier: GetIdentifier;
signMessage: SignMessage;
getBlockNumber?: GetBlockNumber;
getChainId: GetChainId;
}
Signer
= {
type: "EOA"
type
: "EOA",
getIdentifier: GetIdentifier
getIdentifier
: () => ({
Identifier.identifier: string
identifier
:
const account: {
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
signAuthorization: (parameters: AuthorizationRequest) => Promise<SignAuthorizationReturnType>;
signMessage: ({ message }: {
message: SignableMessage;
}) => Promise<Hex>;
signTransaction: <serializer extends SerializeTransactionFn<TransactionSerializable> = SerializeTransactionFn<TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: {
serializer?: serializer | undefined;
} | undefined) => Promise<Hex>;
signTypedData: <const typedData extends TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: TypedDataDefinition<typedData, primaryType>) => Promise<Hex>;
publicKey: Hex;
source: "privateKey";
type: "local";
}
account
.
address: `0x${string}`
address
,
Identifier.identifierKind: IdentifierKind
identifierKind
:
enum IdentifierKind
IdentifierKind
.
function (enum member) IdentifierKind.Ethereum = 0
Ethereum
,
}),
signMessage: SignMessage
signMessage
: async (
message: string
message
) =>
function hexToBytes(hex_: Hex, opts?: HexToBytesOpts): ByteArray

Encodes a hex string into a byte array.

@paramhex Hex string to encode.

@paramopts Options.

@returnsByte array value.

@example import { hexToBytes } from 'viem' const data = hexToBytes('0x48656c6c6f20776f726c6421') // Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])

@example import { hexToBytes } from 'viem' const data = hexToBytes('0x48656c6c6f20776f726c6421', { size: 32 }) // Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

hexToBytes
(await
const account: {
address: Address;
nonceManager?: NonceManager | undefined;
sign: (parameters: {
hash: Hash;
}) => Promise<Hex>;
signAuthorization: (parameters: AuthorizationRequest) => Promise<SignAuthorizationReturnType>;
signMessage: ({ message }: {
message: SignableMessage;
}) => Promise<Hex>;
signTransaction: <serializer extends SerializeTransactionFn<TransactionSerializable> = SerializeTransactionFn<TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: {
serializer?: serializer | undefined;
} | undefined) => Promise<Hex>;
signTypedData: <const typedData extends TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: TypedDataDefinition<typedData, primaryType>) => Promise<Hex>;
publicKey: Hex;
source: "privateKey";
type: "local";
}
account
.
signMessage: ({ message }: {
message: SignableMessage;
}) => Promise<Hex>
signMessage
({
message: SignableMessage
message
})),
};
const
const client: Client<BuiltInContentTypes>
client
= await
class Client<ContentTypes = BuiltInContentTypes>

Client for interacting with the XMTP network

Client
.
Client<ContentTypes = BuiltInContentTypes>.create<[]>(signer: Signer, options: (Omit<NetworkOptions & DeviceSyncOptions & ContentOptions & StorageOptions & OtherOptions, "codecs"> | Omit<{
backend: Backend;
} & DeviceSyncOptions & ContentOptions & StorageOptions & OtherOptions, "codecs">) & {
codecs?: [] | undefined;
}): Promise<Client<BuiltInContentTypes>>

Creates a new client instance with a signer

@paramsigner - The signer to use for authentication

@paramoptions - Optional configuration for the client

@returnsA new client instance

create
(
const signer: {
type: "EOA";
getIdentifier: GetIdentifier;
signMessage: SignMessage;
}
signer
, {
backendUrl: string

Backend URL, including the HTTP or HTTPS scheme.

backendUrl
: "http://127.0.0.1:5050",
dbEncryptionKey: Uint8Array<ArrayBuffer>
dbEncryptionKey
:
var crypto: Crypto
crypto
.
Crypto.getRandomValues<Uint8Array<ArrayBuffer>>(array: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>

The Crypto.getRandomValues() method lets you get cryptographically strong random values.

MDN Reference

getRandomValues
(new
var Uint8Array: Uint8ArrayConstructor
new (length: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(32)),
dbPath: null
dbPath
: null,
});
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
("Your inbox ID:",
const client: Client<BuiltInContentTypes>
client
.
Client<BuiltInContentTypes>.inboxId: string | undefined

Gets the inbox ID associated with this client

inboxId
);

Create a conversation and send a text message. For Browser and Node, use the inbox ID printed by a second running client on the same backend. For Kotlin and Swift, use the recipient client created in the preceding step.

const
const recipientInboxId: string | null
recipientInboxId
=
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
function prompt(message?: string, _default?: string): string | null

window.prompt() instructs the browser to display a dialog with an optional message prompting the user to input some text, and to wait until the user either submits the text or cancels the dialog.

MDN Reference

prompt
("Recipient inbox ID");
if (!
const recipientInboxId: string | null
recipientInboxId
) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Enter a recipient inbox ID");
const
const group: Group<BuiltInContentTypes>
group
= await
const client: Client<BuiltInContentTypes>
client
.
Client<BuiltInContentTypes>.conversations: Conversations<BuiltInContentTypes>

Gets the conversations manager for this client

conversations
.
Conversations<BuiltInContentTypes>.createGroup(inboxIds: string[], options?: CreateGroupOptions): Promise<Group<BuiltInContentTypes>>

Creates a new group conversation with the specified inbox IDs

@paraminboxIds - Array of inbox IDs for other group members (the creator is included automatically)

@paramoptions - Optional group creation options

@returnsPromise that resolves with the new group

createGroup
([
const recipientInboxId: string
recipientInboxId
]);
await
const group: Group<BuiltInContentTypes>
group
.
Conversation<BuiltInContentTypes>.sendText(text: string, opts?: SendOpts): Promise<string>

Sends a text message

@paramtext - The text to send

@paramopts - Send options (optimistic delivery, idempotency key)

@returnsPromise that resolves with the message ID after it has been sent

sendText
("Hello everyone");

With XMTP, sending a message delivers it to the backend, but doesn’t automatically update the local UI (unless you use optimistic sending).

Use streamAllMessages to receive new messages. Use syncAll to fetch new welcomes, preference updates, conversations, and messages.

const
const stream: MessageStream<DecodedMessage | undefined, DecodedMessage<BuiltInContentTypes>>
stream
= await
const client: Client<BuiltInContentTypes>
client
.
Client<BuiltInContentTypes>.conversations: Conversations<BuiltInContentTypes>

Gets the conversations manager for this client

conversations
.
Conversations<BuiltInContentTypes>.streamAllMessages(options?: (StreamOptions<DecodedMessage, DecodedMessage<BuiltInContentTypes>> & {
conversationType?: ConversationType;
consentStates?: ConsentState[];
groupIds?: string[];
from?: DeliveryCursor;
}) | undefined): Promise<MessageStream<DecodedMessage | undefined, DecodedMessage<BuiltInContentTypes>>>

Reads retained messages after each group's default acknowledgement position. Set from to replay after a cursor without changing default progress. Set onValue for callback mode, or request items with the iterator. Core owns network recovery. Legacy retry options and disableSync do not apply.

@paramoptions - Optional stream options

@paramoptions.conversationType - Optional conversation type to filter messages

@paramoptions.consentStates - Optional consent states to filter messages

@returnsStream instance for new messages

streamAllMessages
({
onValue?: ((value: DecodedMessage<BuiltInContentTypes>) => void | Promise<void>) | undefined

Called when a value is emitted from the stream. For message streams, this selects callback mode. Do not also iterate that stream. Message delivery is acknowledged after this callback returns successfully.

onValue
: (
message: DecodedMessage<BuiltInContentTypes>
message
) =>
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
("New message:",
message: DecodedMessage<BuiltInContentTypes>
message
),
onError?: ((error: Error) => void) | undefined

Called when a stream error occurs

onError
:
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.error(...data: any[]): void (+1 overload)

The console.error() static method outputs a message to the console at the 'error' log level.

MDN Reference

error
,
});
await
const client: Client<BuiltInContentTypes>
client
.
Client<BuiltInContentTypes>.conversations: Conversations<BuiltInContentTypes>

Gets the conversations manager for this client

conversations
.
Conversations<BuiltInContentTypes>.syncAll(consentStates?: ConsentState[]): Promise<void>

Synchronizes all conversations and messages from the network with optional consent state filtering

@paramconsentStates - Optional array of consent states to filter by

@returnsPromise that resolves when sync is complete

syncAll
([
enum ConsentState
ConsentState
.
function (enum member) ConsentState.Allowed = 1
Allowed
]);