Skip to content

Migrate to a self-hosted backend

The new XMTP SDKs use a backend that you operate. The backend does not join or copy the previous XMTP network. All participants in a conversation must use the same deployment. Update the app’s SDK calls, create new local databases, and replace the old push integration.

This guide compares two fixed source versions:

BeforeAfter
main at cc878025daself-hosted at 610175f3b3

These are source baselines, not one shared release number. The examples marked Before use the old SDK. The examples marked After use the new SDK. Do not mix imports or generated runtime assets from the two versions. React Native 8.0.0-rc4 is included; every pre-8.0.0 React Native app must migrate.

Use 8.0.0-rc4 for Browser, Node, React Native, Kotlin, Swift, Agent, and the backend. Pin the exact package version in your app and deployment configuration.

ComponentPackage or imageRecommended version
Browser@xmtp/browser-sdk8.0.0-rc4
Node@xmtp/node-sdk8.0.0-rc4
React Native@xmtp/react-native-sdk8.0.0-rc4
Androidorg.xmtp:android8.0.0-rc4
iOSSwift package product XmtpSdk from xmtp/libxmtp8.0.0-rc4 (tag ios-8.0.0-rc4)
Agent@xmtp/agent-sdk8.0.0-rc4
Backendghcr.io/xmtp/backend8.0.0-rc4

See Install the XMTP SDK for the exact package commands. Run the backend with ghcr.io/xmtp/backend:8.0.0-rc4; see Run the backend.

  1. Deploy the backend and configure its URL, authentication, retention, limits, and optional attachment and push services.
  2. Install the matching SDK. An older network SDK cannot use this backend.
  3. Preserve required history with the old SDK. Keep a recoverable copy of each database and its encryption key before replacing it.
  4. Choose your SDK in SDK Upgrade Guides. Update the signer, client options, method calls, and result handling.
  5. Register users and create conversations on the new backend. Test sending, reading, restart, stream recovery, and push with two installations before moving users.

Pass an explicit backend URL through ClientOptions.backend. Include the scheme: https:// for a public deployment, or http:// for local development. There is no default network or fallback endpoint. Remove the old apiUrl, gatewayHost, and mobile isSecure options.

Use StorageOptions to select the database location and optional label. A label names a directory component; it does not select a network. Its default is absent. The SDK rejects unsafe path components. The Agent SDK retains XMTP_ENV as a storage label and uses XMTP_BACKEND_URL for the endpoint.

For a directory location, storage contains an optional label directory, then a deployment directory and an inbox directory. Keep the database, attachment directory, and encryption key together. Browser storage does not accept a database encryption key. Native storage accepts an optional 32-byte key.

Backend authentication is separate from the signer used for XMTP identity. Supply a credential source, or supply a credential and update it with setCredential. The credential has a header value, an expiry in Unix seconds, and an optional header name. The default header is authorization. For bearer authentication, include Bearer and one space before the API key or JWT.

The SDK caches callback credentials and uses bounded recovery after expiry or rejection. A direct credential has no refresh callback; the app must replace it. Configuration discovery does not use credentials or call the credential source. See Backend authentication for each SDK’s credential shape.

fetchServerConfiguration takes a backend source and needs no client or database. Each client keeps the configuration snapshot captured when it was built. Read client.serverConfiguration in JavaScript, or client.serverConfiguration() on mobile. refreshServerConfiguration() stores configuration for the next build; it does not replace the current client’s snapshot.

Online creation fetches configuration when no cached copy exists or the URL changes. Background refresh runs about hourly. The deployment supplies group and installation limits; the defaults are 250 group members and 10 installations per inbox.

Handle typed XmtpError variants in JavaScript and Swift, or XmtpException variants in Kotlin. The SDK-specific sections show their shapes. Preserve ErrorDetails, including its code, category, retry flag, and any stream failure.

A database is bound to a backend identifier. Its URL can change while the identifier stays the same. A different identifier records a persistent conflict and stops backend operations. A later matching refresh does not clear that conflict. Use a database created for the intended deployment.

A database created before the transition is rejected with this error:

This database predates the backend transition. Delete the database file and create a new client.

It is a storage error whose details.code is StorageError::PreTransitionDatabase. Match the code, not the message. The error is not retryable. The SDK leaves the database intact. Close the old client before file operations. Use prepareMigrationArchive from the normal SDK package to convert a supported legacy database into an encrypted history archive. The converter uses a temporary copy and does not need a client or network access. Import that archive through the new client’s archives resource. Then use a new storage location, or remove the old database through the app’s migration flow. The normal client cannot open a legacy database directly.

Browser readMigrationArchive loads the completed archive into memory. It accepts up to 100 MB (100,000,000 bytes), inclusive, and returns MigrationOutput for a larger archive before allocating its bytes. The per-record limit is separate.

A new database creates a new installation. Register it on the new backend. After migration, preserve the database and its key between launches to avoid creating more installations and reaching the deployment’s limit.

Await client shutdown with end() before external database operations. Browser storage tools are now available through Storage.admin(). Import requires an absent destination and a current database. It does not overwrite an existing file or convert a pre-transition database. Import changes the database identity and invalidates earlier delivery cursors and acknowledgement tokens.

You can import messages and conversations from a trusted database file created by a supported older SDK. Close the old SDK, keep a recoverable copy of the database and its encryption key, and use prepareMigrationArchive to create an encrypted archive. Import that archive through the new client’s archives resource. Only import history from a database you trust.

Imported conversations are inactive. You can read their message history, but you cannot send or receive new messages in them. Importing history does not move contacts or active conversations to the new network.

Before you can message your old contacts, they must join the same new self-hosted network. Once they have joined, create new conversations with them on that network.

Every archive method requires a separate 32-byte archive key. See Backups for import methods and key handling.

Message readers can deliver retained messages when they open, then continue with new messages. Do not assume that a stream contains only new traffic. Make app effects safe to repeat.

ConsumerAcknowledgement boundary
JavaScript or Swift async iteratorThe next read acknowledges the previous item.
JavaScript onValue callbackThe stream advances after the awaited callback returns successfully.
Kotlin FlowThe direct collector returns before the next read. A buffer changes this boundary.

Closing does not acknowledge the last item. A crash before acknowledgement can cause repeat delivery. Await work that must finish before acknowledgement. Custom codec decode errors remain on delivered messages with their original bytes and error details; they do not silently remove the message.

One default message reader owns delivery progress for a database. An explicit delivery cursor opens an independent reader without advancing that default progress. A history snapshot provides a boundary between local history and subsequent delivery. Cursors are local to their database.

Reader selection stays fixed for the reader’s lifetime. Open a new reader to change it. JavaScript’s old retry callbacks and disableSync options are removed. The core handles bounded recovery. A terminal error ends the reader; the app can open a replacement on the same client. Notification streams have no separate pre-sync. Use an explicit sync when the app needs a current snapshot first.

Message streams perform network catch-up and local processing. Apps that keep a message stream open do not need repeated manual sync calls to receive messages. Explicit sync, conversations.syncAll, and catchUpToLive remain useful for a fixed read or a background handler. A completed catch-up can report rejected envelopes: its failed count and completed flag describe separate results. An incomplete barrier throws an error with the partial summary in its stream failure details. See Sync and Cursors.

If a send returns PublishedButUnconfirmed, sync the conversation to finish confirmation. Do not send the message again.

Replace the separate push server, manual topic subscriptions, and HMAC-key updates with the SDK’s notification APIs. Node, React Native, Kotlin, and Swift expose enableNotifications, notificationState, and disableNotifications. Agents use agent.client. Browser does not expose these methods.

The SDK maintains subscriptions and registration renewal while its task runner runs. Use conversation.setNotifications for overrides. Read the effective value from the state snapshot: group.state().common.notificationsEnabled or dm.state().notificationsEnabled, with the required await or try for your SDK. Re-register after migration; old-server registrations and overrides are not imported.

Configure the backend’s push channels. APNs needs the operator’s key, team ID, key ID, bundle ID, and environment. FCM needs a Firebase service account. Apps supply their provider token, or an HTTPS receiver URL and a random 32-byte signing key. Registration fails if the selected channel is not configured. Retire the old push server when app migration is complete; the new disableNotifications cannot remove its registrations.

Push payloads contain a base64 binary topic and decimal-text sequence_id, with no message bytes. APNs sends a background notification; FCM sends a data message. HTTPS also includes recipient_id and signed Standard Webhooks headers.

Treat a push as a hint. Open the correct database, process pending Welcomes, sync the conversation, and read decrypted messages. Apply consent and sender checks and suppress duplicates before showing a notification. Delivery can be delayed, repeated, or absent. See Push notifications for complete payload routing and handler examples.

The SDK’s display topic, such as [group_message_v1/<hex-id>], differs from the base64 push field. Neither uses the old /xmtp/mls/1/g-<id>/proto routing label. Parse push payloads with the SDK’s parsePushHint; the topic layout describes the bytes.

Choose the SDK used by your app. Each guide lists only changes to calls, options, result types, or behavior. Apply the shared migration steps above before you update the SDK.