Follow this tutorial to run a local XMTP backend and send a message from one
client to another.
Each client uses a key pair to create an identity and register it with the
backend. This gives the client an inbox ID, which is a stable identifier that
other clients send messages to.
You need Docker with Docker Compose. For
Browser and Node, you also need Node.js 22.18.0 or later. For React Native,
use the native app tooling requirements.
The last command returns when the backend is healthy. The backend URL is
http://127.0.0.1:5050. Use 127.0.0.1, not localhost: some systems resolve
localhost to an IPv6 address that Docker does not forward.
The Compose file
compose.yaml
# Local XMTP backend for the quickstart. Do not use it in production:
# it has no authentication and a fixed database password.
#
# docker compose up -d --wait start, and wait until the backend is healthy
# docker compose logs backend show the backend logs
# docker compose down stop, and keep the data
# docker compose down -v stop, and delete the data
Use SDK 8.0.0-rc2 for Browser, Node, Kotlin, and Swift. React Native uses
8.0.0 with the same backend image. See
Install the XMTP SDK for other package managers.
In the xmtp-quickstart directory, create an ES module project and install the
SDK:
Terminal window
npminit-y
npmpkgsettype=module
npminstall--save-exact@xmtp/node-sdk@8.0.0-rc2
Follow React Native and Expo setup. Add
@xmtp/react-native-sdk@8.0.0 to a native app with Hermes and New Architecture,
install iOS pods, and rebuild the native app. Use an Expo development build;
Expo Go cannot load the module.
Replace the contents of src/main.ts with this code. Each page load creates a
new client. A page opened with ?to=<inbox-id> sends one message to that inbox.
src/main.ts
import {
classClient
An XMTP client. Its inputs and results are public values; the binding
Client and the target transport stay private to the package.
Client } from"@xmtp/browser-sdk";
const
constlog: (line:string) =>void
log= (
line: string
line:string) => {
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
The search property of the Location interface is a search string, also called a query string, that is a string containing a "?" followed by the parameters of the location's URL. If the URL does not have a search query, this property contains an empty string, "".
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
@since ― v0.1.27
env.
string |undefined
XMTP_BACKEND_URL??"http://127.0.0.1:5050" },
storage: StorageOptions
storage: {
location: StorageLocation
location: "inMemory" },
});
var console:Console
console.
Console.log(...data: any[]): void
The console.log() static method outputs a message to the console.
Create send.ts. It creates a second client and sends one message to the inbox
ID you pass to it:
send.ts
import {
classClient
An XMTP client. Its inputs and results are public values; the binding
Client and the target transport stay private to the package.
Client } from"@xmtp/node-sdk";
const [
constinboxId:string
inboxId,
consttext:string
text="Hello from XMTP"] =
var process:NodeJS.Process
process.
NodeJS.Process.argv: string[]
The process.argv property returns an array containing the command-line
arguments passed when the Node.js process was launched. The first element will
be
execPath
. See process.argv0 if access to the original value
of argv[0] is needed. The second element will be the path to the JavaScript
file being executed. If a program entry point was provided, the second element
will be the absolute path to it. The remaining elements are additional command-line
arguments.
For example, assuming the following script for process-args.js:
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.
@param ― start The beginning index of the specified portion of the array.
If start is undefined, then the slice begins at index 0.
@param ― end The end index of the specified portion of the array. This is exclusive of the element at the index 'end'.
If end is undefined, then the slice extends to the end of the array.
slice(2);
if (!
constinboxId:string
inboxId) thrownew
var Error:ErrorConstructor
new (message?:string, options?:ErrorOptions) =>Error (+1 overload)
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
import { env } from'node:process';
env.test =null;
console.log(env.test);
// => 'null'
env.test =undefined;
console.log(env.test);
// => 'undefined'
Use delete to delete a property from process.env.
import { env } from'node:process';
env.TEST=1;
delete env.TEST;
console.log(env.TEST);
// => undefined
On Windows operating systems, environment variables are case-insensitive.
import { env } from'node:process';
env.TEST=1;
console.log(env.test);
// => 1
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
End the client. Messages from it then fail with ClientClosed. Ending a
closed client does nothing, also after its browser worker stopped.
end();
Use this function from your app. Supply a saved signer and 32-byte database key,
a backend URL reachable from the device, and callbacks for incoming text and
stream errors. Save the returned inbox ID to share with the other app.
import {
classClient
An XMTP client. Its inputs and results are public values; the binding
Client and the target transport stay private to the package.
A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the
requested number of bytes could not be allocated an exception is raised.
Uint8Array,
onText: (text:string) =>Promise<void>
onText: (
text: string
text:string) =>
interfacePromise<T>
Represents the completion of an asynchronous operation
Promise<void>,
onError: (error:unknown) =>void
onError: (
error: unknown
error:unknown) =>void,
) {
const
constclient:Client
client=await
classClient
An XMTP client. Its inputs and results are public values; the binding
Client and the target transport stay private to the package.
Await each callback before the next read. For messages, that read acknowledges
the prior message. Await all processing in the callback. Work sent to an app
queue or an unawaited task can continue after acknowledgement.
End the client. Messages from it then fail with ClientClosed. Ending a
closed client does nothing, also after its browser worker stopped.
end();
}
},
};
}
The signer provides identity(), kind(), and sign(request).
SDKClient.generateKeys() returns a local signing key that implements these
methods. This example creates two temporary clients on the same backend, sends a
message from one to the other, and streams messages. Replace the backend URL
with a URL that your device can reach.
The withDemoClients helper ends both clients when its block exits.
The signer provides identity(), kind(), and sign(request).
SDKClient.generateKeys() returns a local signing key that implements these
methods. This example creates two temporary clients on the same backend, sends a
message from one to the other, and streams messages. Replace the backend URL
with a URL that your device can reach.
Load each client’s stored 32-byte database key before the calls below. Generate a
new key securely for a new database, store it securely, and reuse it on reopen.
Omitting the key selects unencrypted storage.
Open the URL that Vite prints, such as http://localhost:5173/. The page
shows its inbox ID.
Open the ?to= link from the first page in a second tab. The second tab
sends Hello from the browser.
The first tab shows Them: Hello from the browser.
In the first terminal, start the listener:
Terminal window
nodelisten.ts
It prints a command with its inbox ID. Run that command in a second terminal
from the same directory:
Terminal window
nodesend.ts<inbox-id>"Hello from XMTP"
The first terminal prints New message: Hello from XMTP. Run send.ts again to
send more messages. Press Ctrl+C to stop the listener.
Build and run the native app on a supported device or simulator. Connect two
app installations to the same reachable backend. Display one installation’s
inboxId and pass it to the other installation’s send action. The receiver’s
onText callback receives the text. Call disconnect when that app owner is
finished. Reuse its signer, database key, and storage on later launches.
Run the app. The stream prints the message that the first client sent.
Run the app. The stream prints the message that the first client sent.
streamAllMessages() delivers messages from every conversation, including
conversations that start after the stream opens. By default, it includes
allowed and unknown conversations. A conversation that another inbox
starts is unknown until you allow it. See Consent.
When the stream opens, it first delivers the messages that arrived while the
client was offline.
The Browser, Node, Kotlin, and Swift examples discard their generated keys and
in-memory databases when the process ends. Each run is a new inbox. The React
Native example uses the app’s saved signer, database key, and default native
storage. Reuse them between launches.
A persistent app must
save and restore its signing key
and keep the same database between launches. In Node, pass a 32-byte
storage.encryptionKey to encrypt the database.