Skip to main content

Android account API

SoftchatAccount is the Android product API. It owns one imported local authority and one identity-derived Rust SQLite database. Product code calls its cohesive groups directly:

account.profile account.conversations
account.messages account.settings
account.relays account.sync
account.media account.push
account.operations account.recovery
account.support

account.incoming and account.transport are system-adapter boundaries for Firebase and the one Android WebSocket executor. Compose code does not assemble events, recipients, relay filters, encryption envelopes, SQL, or retry policy.

Android continues to own Keystore, lifecycle, Ktor, WorkManager, content URIs, private files, notifications, and UI. Every product query is bounded, every mutation is durable before its external effect, and each Flow reruns a complete query after the Rust revision changes.

Open an account

Operation contractaccount.open

Import one local authority and open its identity-derived Rust SQLite account.

Inputs and exact bounds
  • exactly 32 secret bytes
  • existing absolute app-private directory
  • at most one open handle for the canonical file in the process
Output
One account capability, canonical public key, and deterministic database filename.
Trust and authentication
Rust derives and verifies the database binding; the mutable caller buffer is cleared.
Stable errors
invalid_secret_key, invalid_account_operation, account_already_open, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::openAccountRuntimeHandle::public_keyAccountRuntimeHandle::close_account
let account = AccountRuntimeHandle::open(secret_bytes, private_directory)?;
let author = account.public_key()?;
let filename = account.database_filename();
account.close_account();

Rust derives the account ID and filename from the authority. The caller cannot pair an arbitrary account ID with a secret. open clears the supplied mutable buffer on success and failure; close blocks new calls, waits for active calls, closes SQLite, and erases in-memory authority.

Conversations and drafts

Operation contractaccount.conversations

Create, query, observe, archive, pin, read, and draft complete conversations.

Inputs and exact bounds
  • 1 through 256 unique remote member keys
  • page limit from 1 through 100
  • stable tuple cursor
  • at most 256 valid UTF-16 draft spans and 32 attachment operation references
Output
Complete stable conversation pages with subject, last message, unread state, and portable draft.
Trust and authentication
Protocol fields derive only from authenticated truth; archive, pin, read, and draft fields are explicit account state.
Stable errors
invalid_public_key, invalid_account_operation, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::get_or_create_conversationAccountRuntimeHandle::conversation_pageAccountRuntimeHandle::save_conversation_draft
let conversation =
account.get_or_create_conversation(vec![bob_public_key], now)?;
let page = account.conversation_page(false, None, 50)?;
account.set_conversation_pinned(conversation.id.clone(), true, now)?;

Conversation pages already contain subject, last message, archive/pin state, tuple read boundary, forced-unread state, unread count, draft, and stable ordering. Render the returned value; do not issue a per-row query or maintain a second conversation cache.

Messages and commands

Operation contractaccount.messages

Query effective messages and execute atomic send, reply, forward, subject, reaction, edit, deletion, and typing use cases.

Inputs and exact bounds
  • command ID from 1 through 128 bytes
  • stored conversation or message ID
  • message text at most 64 KiB
  • page limit from 1 through 100
Output
Effective message pages and idempotent recipient-complete outgoing operations.
Trust and authentication
Rust loads and authorizes every referenced parent, member set, relation, and active relay from authenticated storage.
Stable errors
command_conflict, invalid_account_operation, invalid_delivery_state, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::send_messageAccountRuntimeHandle::message_pageAccountRuntimeHandle::edit_messageAccountRuntimeHandle::delete_message
let result = account.send_message(
command_id,
conversation_id,
MessageContentInput {
text: "hello".into(),
attachments: Vec::new(),
emoji_tags: Vec::new(),
},
String::new(),
now,
)?;
let page = account.message_page(conversation_id, None, 50)?;

Rust loads the stored parent, author, members, active relay, relation, and authorization. Edits accept replacement text and emoji tags only; attachments remain immutable. Repeating the same command ID with the same input returns the same logical operation and recipient copies. Reusing it for different input is a permanent conflict.

Profile and follows

Operation contractaccount.profile

Observe effective private metadata and update profile fields or the complete follow list without lossy replacement.

Inputs and exact bounds
  • typed three-state profile patch
  • at most 10,000 unique follows
  • stable command ID
Output
Authenticated effective profile and follow views plus idempotent outgoing operations.
Trust and authentication
Rust preserves authenticated and unknown profile fields not changed by the patch.
Stable errors
command_conflict, invalid_user_metadata, invalid_contact_list, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::profileAccountRuntimeHandle::update_profileAccountRuntimeHandle::replace_follows
let current = account.profile(account.public_key()?)?;
let updated = account.update_profile(command_id, patch_json, now)?;
let follows = account.follows()?;

Use ProfilePatch; do not construct a complete kind-0 replacement. Rust preserves authenticated fields and unknown JSON that the editing screen did not change. Follow replacement is private, author-bound, idempotent, and includes the account copy required for cross-device recovery.

Account settings

Operation contractaccount.settings

Read and merge versioned cross-device product settings through explicit unchanged, clear, and set states.

Inputs and exact bounds
  • stable command ID
  • patch JSON at most 64 KiB
  • validated HTTPS references and bounded typed fields
Output
Complete effective settings and a durable private app-data synchronization operation.
Trust and authentication
Rust validates known fields and preserves unknown fields from newer schema versions.
Stable errors
command_conflict, invalid_account_settings, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::account_settingsAccountRuntimeHandle::apply_account_settings_patch
let settings = account.account_settings()?;
let updated =
account.apply_account_settings_patch(command_id, patch_json, now)?;

Every field is a three-state patch: unchanged, clear, or set. Rust validates URLs and bounds, preserves fields from newer schema versions, stores the effective settings, and queues the private cross-device app-data update in the same coarse operation.

Relay catalog

Operation contractaccount.relays

Own the durable relay catalog, one active selection, discovery reconciliation, and failover order.

Inputs and exact bounds
  • normalized secure WebSocket URL
  • bounded authenticated DNS results
  • one selected effective relay
Output
Complete catalog state and deterministic transport candidates.
Trust and authentication
DNS authentication state and user ownership remain explicit; Android does not invent relay policy.
Stable errors
invalid_relay_catalog, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::relay_catalogAccountRuntimeHandle::add_account_relayAccountRuntimeHandle::set_account_active_relay
let catalog = account.relay_catalog()?;
account.add_account_relay("wss://relay.example".into(), now)?;
account.set_account_active_relay("wss://relay.example".into(), now)?;

Rust owns validation, discovery reconciliation, active selection, retry delay, and failover ordering. Android may report DNS/DNSSEC and connectivity results, but it opens only the endpoint requested by account.transport.

Route an incoming account event

Operation contractaccount.route-incoming

Verify one signed NIP-59 outer wrapper and select the exact retained account before decryption or ingestion.

Inputs and exact bounds
  • one bounded signed event JSON
  • durable kind 1059 or ephemeral kind 21059
  • exactly one canonical recipient p tag
Output
Verified outer event ID, canonical recipient public key, and envelope kind.
Trust and authentication
The route authenticates only the outer wrapper; inner content remains untrusted until the selected account ingests and authenticates every layer.
Stable errors
invalid_event_json, invalid_event_signature, invalid_event_kind, invalid_event_tag, invalid_nip59_envelope · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
route_nip59_envelopeNip59EnvelopeRoute
let route = softchat::route_nip59_envelope(&event_json)?;
let recipient = route.recipient();
let envelope_kind = route.kind();

// Select the recipient account, then pass the original JSON through that
// account's full authenticated ingestion path.

Routing verifies the signed outer event, supported wrapper kind, and exactly one canonical recipient tag. It does not decrypt or trust the seal or rumor. Keep the exact original JSON and pass it to the selected account; display only changes returned after that account commits the fully authenticated envelope. An inactive-account lease must not change the selected UI account or start a second relay socket.

Transport, ingestion, and synchronization

Operation contractaccount.transport

Drive one correlated Rust-owned relay, delivery, ingestion, Noise, and synchronization state machine through Android system effects.

Inputs and exact bounds
  • run and action IDs from 1 through 128 bytes
  • at most 128 actions per batch
  • at most 150 incoming events and 512 KiB per ingestion call
Output
Bounded correlated system actions, complete connection/sync state, and authenticated committed changes.
Trust and authentication
Socket bytes and push JSON are untrusted until Rust validates and commits them; writes are not delivery.
Stable errors
unsupported_system_action, invalid_relay_session, invalid_sync_state, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::start_account_transportAccountRuntimeHandle::apply_account_transport_resultAccountRuntimeHandle::reset_account_sync
let batch = account.start_account_transport(run_id)?;
let next = account.apply_account_transport_result(result, now)?;
let reset = account.reset_account_sync(now)?;

Each action carries a run ID, action ID, and generation. Echo all three in the result. Apply results once; Rust ignores duplicates and stale generations. Receiving a frame is not persistence and writing a frame is not delivery. account.incoming.ingestJson authenticates and commits bounded push or socket events before any UI or notification consumes them.

Durable media

Operation contractaccount.media

Persist media intent and dependencies before Android executes bounded file and HTTP effects.

Inputs and exact bounds
  • at most 32 staged source fingerprints per message
  • authenticated stored message and attachment URL for download
  • lease duration from 1 through 3,600 seconds
Output
Recoverable media operations with exact source-message correlation for downloads, exclusive leases, final authenticated metadata, and gallery views.
Trust and authentication
Decrypted output remains private and untrusted until final Rust authentication succeeds.
Stable errors
invalid_media_operation, invalid_attachment_metadata, attachment_decryption_failed, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::prepare_media_messageAccountRuntimeHandle::prepare_media_downloadAccountRuntimeHandle::claim_media_operation
let pending = account.prepare_media_message(
command_id,
conversation_id,
content,
String::new(),
sources,
now,
)?;
let operations = account.media_operations(50)?;

Android privately stages bytes before committing an operation. WorkManager claims a durable lease, streams encryption/decryption, and reports only final validated metadata or a redacted failure category. Decrypted bytes remain private and untrusted until final authentication succeeds; verified cache eviction restarts the same logical download. Each download carries its exact authenticated sourceMessageId, so UI and cache state are never correlated by URL alone.

Durable public asset replacement

Operation contractaccount.asset-replacement

Persist a profile-picture, profile-banner, or conversation-icon replacement and its upload dependency before Android starts file or HTTP work.

Inputs and exact bounds
  • command ID from 1 through 256 bytes
  • private staged source fingerprint from 1 through 512 bytes
  • stored conversation ID for an icon replacement
  • validated public attachment metadata without an encryption descriptor
Output
One idempotent public upload operation and, after completion, one durable profile or conversation-subject operation retaining the latest unrelated fields.
Trust and authentication
The selected source stays app-private while staged, but the uploaded object and resulting URL are intentionally public; Rust rejects encryption metadata and publishes only after the matching lease completes.
Stable errors
command_conflict, invalid_account_operation, invalid_media_operation, invalid_attachment_metadata, invalid_user_metadata, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::prepare_asset_replacementAccountRuntimeHandle::pending_asset_replacementAccountRuntimeHandle::recover_pending_asset_replacements
let pending = account.prepare_asset_replacement(
command_id.clone(),
AssetReplacementTarget::ProfilePicture,
String::new(),
staged_source_fingerprint,
now,
)?;
assert_eq!(
pending.operation.protection,
AccountMediaProtection::Public,
);

// The platform claims and uploads the exact operation without encryption.
let lease = account
.claim_media_operation(
pending.operation.id.clone(),
lease_id,
now,
300,
)?
.expect("operation is claimable");
let completion = account.complete_media_operation(
lease.operation.id,
lease.lease_id,
public_attachment_metadata,
plaintext_byte_count,
now,
)?;
assert!(completion.completed_operation.is_some());

// Safe after a crash between upload completion and dependent publication.
account.recover_pending_asset_replacements(50, now)?;

Profile pictures, banners, and conversation icons are intentionally public objects: other clients receive their URL and optional dimensions, not attachment decryption material. The Android worker therefore copies the privately staged source into its upload body without attachment encryption and must complete the lease with AttachmentMetadata.encryption == null. Ordinary message attachments continue to use MediaProtection.ENCRYPTED.

The SDK inserts the asset intent and public upload operation in one transaction. Reusing the same command ID with the same target and source returns the same operation; changing any of those inputs produces command_conflict. Completion atomically marks the upload complete and authors the dependent profile or subject operation. For a conversation icon, Rust reloads the latest subject and emoji tags at completion time, so an edit made while the upload is running is not overwritten.

Only one valid lease may complete an operation. Report temporary failures with failRetryable, permanent failures with failPermanent, and user cancellation with cancel(pending.operation.id). A cancelled or permanently failed upload propagates to the aggregate asset state. If the process dies after upload completion but before publication, recoverReadyAssetReplacements finishes the exact dependent command idempotently.

This use case has no accepted standalone benchmark yet. It performs one bounded SQLite transaction at preparation and one at completion around Android-owned file and HTTP work; no numeric result is copied here until it is present in the accepted performance data.

Delivery operations

Operation contractaccount.operations

Inspect, retry, and safely cancel recipient-complete durable outgoing operations.

Inputs and exact bounds
  • stable command ID
  • page limit from 1 through 100
Output
Aggregate state plus independent terminal state for every recipient copy.
Trust and authentication
Only correlated relay acknowledgements advance delivery; a socket write never does.
Stable errors
invalid_delivery_state, invalid_account_operation, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::operationAccountRuntimeHandle::operation_pageAccountRuntimeHandle::retry_operation
let operation = account.operation(command_id.clone())?;
let page = account.operation_page(50)?;
let retried = account.retry_operation(command_id)?;

Operation state is recipient-complete. A socket write does not mark delivery; every recipient copy reaches a terminal relay acknowledgement independently. Retry requeues only terminally rejected copies, and cancel affects only copies that are still safe to cancel.

Push registration

Operation contractaccount.push

Create the released private FCM or APNs token registration profile and durable delivery intent.

Inputs and exact bounds
  • stable installation-derived command ID
  • bounded platform token
  • bounded non-secret installation ID
Output
An idempotent private outgoing operation.
Trust and authentication
The platform supplies token and installation identity; Rust owns wire profile, recipients, encryption, and delivery.
Stable errors
command_conflict, invalid_account_operation, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::register_push_token
let registration = account.register_push_token(
command_id,
PushPlatform::Fcm,
token,
installation_id,
now,
)?;

The installation store is Android-owned. Rust validates the token and creates the released private registration profile, recipient copies, and delivery intent. Reuse the stable installation-derived command ID for the same token.

Local contacts and resources

Operation contractaccount.contacts-resources

Store bounded account-local address-book labels, stickers, and link-preview cache values in the one Rust database.

Inputs and exact bounds
  • canonical public key
  • at most 200 unique sticker records
  • validated HTTP(S) URLs and bounded preview fields
Output
Observed local contacts and validated resource cache values.
Trust and authentication
These values are local or HTTP-derived and never impersonate authenticated protocol truth.
Stable errors
invalid_public_key, invalid_account_operation, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::put_local_contactAccountRuntimeHandle::replace_stickersAccountRuntimeHandle::put_link_preview
account.put_local_contact(contact)?;
account.replace_stickers(stickers)?;
account.put_link_preview(preview)?;

Contacts are local labels, never authenticated identity. Stickers and link previews are bounded untrusted cache values. Keeping them in the one Rust database avoids a second protocol-adjacent Room or key-value store without changing their trust level.

Import a retired signed outbox

Operation contractaccount.recovery

Recover a bounded batch of already signed pending events from retired platform storage without importing its schema or projections.

Inputs and exact bounds
  • stable command ID
  • one non-empty JSON array at most 512 KiB
  • at most 512 complete signed events
Output
One idempotent durable outgoing operation using the account's active relay.
Trust and authentication
The host treats the old row as opaque; Rust verifies every event ID and signature and commits the complete batch or nothing.
Stable errors
command_conflict, invalid_account_operation, invalid_event_json, invalid_event_id, invalid_event_signature, invalid_event_kind, invalid_event_timestamp, invalid_event_tag, invalid_public_key, invalid_relay_catalog, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::recover_signed_outbox_batch
let recovered = account.recover_signed_outbox_batch(
stable_legacy_row_id,
opaque_event_array_json,
now,
)?;

This is a narrow installed-upgrade boundary, not a general persistence adapter. Android may read an old outbox row from its retired database in read-only mode, apply host-side aggregate bounds, and pass the JSON array without decoding protocol fields. Rust bounds the complete input, verifies every event ID and signature, rejects duplicate events, selects the current active relay, and commits the whole outgoing operation atomically.

Keep the CommandId stable for the old row. Repeating the same row returns the existing operation; reusing that ID for different verified events returns command_conflict. Validation failures are permanent for that row. A database or active-relay failure is retryable only after its underlying condition changes. Retain the old database until the application has reported the per-row result and the product's rollback window has closed.

No accepted standalone benchmark exists for this one-shot migration method. It performs bounded signature verification followed by one SQLite transaction; numeric results will appear here only after a key is added to the accepted performance data.

Diagnostics and recovery

Operation contractaccount.support

Observe the durable revision and bounded redacted account health for explicit support and recovery flows.

Inputs and exact bounds
  • no protocol payload input
Output
Schema, revision, active relay, bounded counts, and database allocation.
Trust and authentication
Diagnostics omit keys, plaintext, ciphertext, raw events, attachment authority, and cryptographic detail.
Stable errors
account_closed, invalid_persistence_result · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics and Android account SQLite/use-case state; the host owns operating-system I/O, lifecycle, scheduling, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AccountRuntimeHandle::account_diagnostics
let diagnostics = account.account_diagnostics()?;
let revision = diagnostics.revision;

Diagnostics contain bounded counts, schema/revision, active relay, and database allocation only. They never include plaintext, ciphertext, keys, event JSON, or attachment authority.

Android ownership checklist

  • Keep the account secret only in an OS-protected credential store.
  • Keep one interactive account and reference-counted short-lived leases for exact inactive-account system work.
  • Call product groups directly; do not add a repository or converter layer.
  • Keep installed-upgrade readers narrow, read-only, bounded, and temporary; pass opaque signed outbox batches to account.recovery.
  • Execute only incoming, transport, media leases, and push registration from Android system adapters.
  • Use bounded Flow queries in Compose and retain CommandId across a retry.
  • Keep content URIs, WorkManager IDs, sockets, HTTP bodies, and cache paths out of portable SDK state.
  • Close the account before switch or deletion, and delete Android-owned media work/cache with the account.

The exact symbols are in the Android reference. The Rust SQLite and transaction rationale records the database alternatives and invariants.