---
sdk = "android"
previous_release_version = "4.10.0"
previous_release_tag = "android-4.10.0"
---

# Android SDK 4.11.0

## Highlights

- **Streams now follow your app's lifecycle automatically.** Backgrounding your app parks the shared streaming connection; foregrounding revives it. No code changes required.
- **New `catchUpToLive()`** for background jobs and cold start — a bounded, one-shot sync that brings the local store current and stops, instead of holding a live stream you are about to lose.
- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate.
- **In-memory clients** via `Client.createInMemory(account, options)` for tests and ephemeral flows.
- **Group metadata migration read APIs**: `proposalsEnabled()` and `membershipCapabilities()`.

This release contains breaking changes. Read the Breaking Changes section before upgrading.

## What's Changed

This release focuses on connection and lifecycle behavior. The two API-client connections libxmtp used to open per client have been collapsed into one, the `isSecure` option is now unused, and the streaming transport is shared process-wide and kept in step with app foreground/background transitions via `ProcessLifecycleOwner`.

Alongside that, sends gained caller-settable idempotency keys, `ClientOptions` reorganized its database pool settings into a `DbPoolOptions` object and gained an option to wait for registration to become visible on the network, and groups gained read APIs for the in-progress migration to proposal-based metadata updates.

Several reliability fixes land here too: a crash on the welcome stream after a group creator left and was re-added, false-positive fork detection after member removal, missed messages in streams following a recoverable error, and dropped group invites for members using smart contract wallets.

The library now depends on `androidx.lifecycle:lifecycle-process` for stream lifecycle management.

## Breaking Changes

### `ClientOptions.maxDbPoolSize` and `minDbPoolSize` moved into `dbPoolOptions`

**This is a breaking change.**

```kotlin
// Before
ClientOptions(..., maxDbPoolSize = 25u, minDbPoolSize = 5u)

// After
ClientOptions(..., dbPoolOptions = DbPoolOptions(maxPoolSize = 25u, minPoolSize = 5u))
```

### `Client.connectToSyncApiBackend(api)` has been removed

**This is a breaking change.**

libxmtp no longer opens a separate sync connection — one API connection now serves both paths. Use `Client.connectToApiBackend(api)`.

### `ClientOptions.Api.isSecure` is deprecated and no longer used

`isSecure` no longer affects the connection and will be removed in a future release. It is also no longer part of the API client cache key. Remove it from your `ClientOptions.Api` constructor calls.

## New Features

### Automatic stream lifecycle management

The streaming connection is shared across every `Client` in your process. The SDK now registers a `ProcessLifecycleOwner` observer on the first client you create, parks that connection when the app backgrounds, and revives it on foreground.

This is on by default. To manage it yourself, set the process-global flag **before creating your first client**:

```kotlin
Client.manageStreamLifecycle = false
```

A process started straight into the background (a service, a broadcast receiver, a WorkManager job) seeds the backgrounded state, so a stream opened there is born parked rather than opening a live connection you cannot use. One caveat: the seed lands a few async hops after the first client is created, so a stream opened immediately in that cold-start window can still briefly open live. Background work that only needs data should prefer `catchUpToLive()`, which uses its own connection.

### `catchUpToLive(timeoutMs)`

Bring the local store current with the network, then stop. Intended for background jobs and cold start, where holding a live stream is wasted work.

```kotlin
val summary = client.catchUpToLive(timeoutMs = 20_000)
Log.d("XMTP", "${summary.messages} messages, ${summary.conversations} conversations, completed=${summary.completed}")
```

Pass `timeoutMs` to bound the run against a background budget (a WorkManager job, an FCM handler); `null` runs to the live edge. Cutting it short is safe — everything processed is persisted and a later call resumes from durable state. Check `summary.completed` before treating the counts as complete: on the deadline path it is `false` and the counts may undercount what was actually stored.

### Idempotency keys on sends

`SendOptions` and `MessageVisibilityOptions` accept an `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted.

```kotlin
conversation.send(
    content = "hello",
    options = SendOptions(idempotencyKey = "my-stable-key"),
)
```

### In-memory clients

```kotlin
val client = Client.createInMemory(account = signer, options = options)
```

No `.db3` file is created and no directory is touched. `dbPath` is `":memory:"`, `isInMemory` returns `true`, and `deleteLocalDatabase()`, `dropLocalDatabaseConnection()`, and `reconnectLocalDatabase()` are no-ops. `options.dbDirectory` and `options.dbEncryptionKey` are ignored. Intended for tests and ephemeral flows.

### Wait for registration to become visible on the network

`ClientOptions.waitForRegistrationVisible` accepts `VisibilityConfirmationOptions(quorumPercentage, quorumAbsolute, timeoutMs)`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation. Also available on `ffiRegisterIdentity(signatureRequest, visibilityConfirmationOptions)` if you drive registration yourself.

### Group metadata migration read APIs

- `group.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates.
- `group.membershipCapabilities()` — a snapshot of the group context's extension types plus, per member inbox and installation, the extension types each advertises. Use it to find which member is blocking migration.

### `group.unstable.enableProposals()` (pre-release)

Staging the migration itself is available, but only through a deliberate opt-in: it lives on `group.unstable` behind `@UnstableApi`.

```kotlin
@OptIn(UnstableApi::class)
suspend fun migrate(group: Group) {
    group.unstable.enableProposals()
}
```

The migration is one-way and irreversible — a migrated group cannot return to the legacy metadata path — and it hard-fails if any member's latest key package does not advertise the required proposal type (pass `force = true` to skip that pre-flight scan). When the API stabilizes it moves onto `Group` directly and the opt-in stops resolving, so you get a compile-time migration signal.

### Native log level control

`Client.setLibXMTPNativeLogLevel(logLevel)` sets the level for the native logcat layer independently of the persistent file log writer. Use `FfiLogLevel.TRACE` to capture span and activity events.

## Bug Fixes

- **Crash on the welcome stream** after a group's creator left the group and was later re-added.
- **False-positive fork detection** after a member was removed from a group.
- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered.
- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync.
- **New installations reliably added to your existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group.
- **Association-state cache returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch inbox/installation lookup could return unrelated rows.
- **Database disconnect errors** now propagate out of background workers to the supervisor instead of being swallowed.
- **Downgrade resilience for group intents.** Intent rows written by a newer SDK version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group.
- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported.
- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error.
- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing.
- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused.
- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers. Already-processed welcomes log at warn rather than error.
