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 is not included in this repository’s new SDKs.

Use 8.0.0-rc1 for every SDK and the backend. Pin the exact release candidate version in your app and deployment configuration.

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

See Install the XMTP SDK for the exact package commands. Run the backend with ghcr.io/xmtp/backend:8.0.0-rc1; 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.

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.

There is no server-side transfer of identities, groups, or history from the old network. Create active conversations on the new backend.

Local archives use the client’s archives resource. All SDKs support archive bytes. Node, Kotlin, and Swift also support archive files. Every archive method, including metadata reads, requires a separate 32-byte archive key. See Backups for the methods and key handling.

Imported conversations are initially inactive. Their local history is readable, but they have no joined MLS state for sending. On the same backend, an active member can add the new installation so that it can join. Restored DMs also remain inactive until the installation joins; a new DM can later be combined with the restored conversation.

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, 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. Use the topic layout to decode payloads.

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.