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
account.openImport 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
- Rust
- Android
AccountRuntimeHandle::openAccountRuntimeHandle::public_keyAccountRuntimeHandle::close_accountSoftchatAccount.openSoftchatAccount.publicKeySoftchatAccount.close- Rust
- Android
let account = AccountRuntimeHandle::open(secret_bytes, private_directory)?;
let author = account.public_key()?;
let filename = account.database_filename();
account.close_account();
val account = credentialStore.withSecret(reference) { temporarySecret ->
SoftchatAccount.open(
secretKey = temporarySecret,
databaseDirectory = context.noBackupFilesDir,
)
}
val author: PublicKey = account.publicKey
val filename: String = account.databaseFilename
// Close before account switch or deletion.
account.close()
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.
- Rust
- Android
Conversations and drafts
account.conversationsCreate, 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
- Rust
- Android
AccountRuntimeHandle::get_or_create_conversationAccountRuntimeHandle::conversation_pageAccountRuntimeHandle::save_conversation_draftAccountConversations.getOrCreateAccountConversations.observeAccountConversations.saveDraft- Rust
- Android
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)?;
val conversation = account.conversations.getOrCreate(listOf(bobPublicKey))
val page: Flow<ConversationPage> =
account.conversations.observe(archived = false, limit = 50)
account.conversations.setPinned(conversation.id, true)
account.conversations.setArchived(conversation.id, false)
account.conversations.markUnread(conversation.id)
account.conversations.saveDraft(
conversation.id,
ConversationDraft(
text = composerText,
replyTo = repliedMessageId,
),
)
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.
- Rust
- Android
Messages and commands
account.messagesQuery 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
- Rust
- Android
AccountRuntimeHandle::send_messageAccountRuntimeHandle::message_pageAccountRuntimeHandle::edit_messageAccountRuntimeHandle::delete_messageAccountMessages.sendAccountMessages.observeAccountMessages.editAccountMessages.delete- Rust
- Android
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)?;
val commandId = CommandId.random() // retain this value while retrying
val sent = account.messages.send(
commandId = commandId,
conversationId = conversation.id,
content = MessageContent(text = "hello"),
replyTo = repliedMessageId,
)
val messages = account.messages.observe(conversation.id, limit = 50)
account.messages.toggleReaction(
CommandId.random(),
message.id,
Reaction("👍"),
)
account.messages.edit(
CommandId.random(),
message.id,
MessageContent(text = "replacement text"),
)
account.messages.forward(CommandId.random(), message.id, destination.id)
account.messages.delete(CommandId.random(), message.id)
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.
- Rust
- Android
Profile and follows
account.profileObserve 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
- Rust
- Android
AccountRuntimeHandle::profileAccountRuntimeHandle::update_profileAccountRuntimeHandle::replace_followsAccountProfiles.observeAccountProfiles.updateAccountProfiles.replaceFollows- Rust
- Android
let current = account.profile(account.public_key()?)?;
let updated = account.update_profile(command_id, patch_json, now)?;
let follows = account.follows()?;
val profile = account.profile.observe()
account.profile.update(
CommandId.random(),
ProfilePatch(
displayName = SettingPatch.Set("Alice"),
about = SettingPatch.Clear,
),
)
val existing = account.profile.follows()
account.profile.replaceFollows(
CommandId.random(),
existing + Follow(newPublicKey),
)
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.
- Rust
- Android
Account settings
account.settingsRead 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
- Rust
- Android
AccountRuntimeHandle::account_settingsAccountRuntimeHandle::apply_account_settings_patchAccountSettingsService.observeAccountSettingsService.apply- Rust
- Android
let settings = account.account_settings()?;
let updated =
account.apply_account_settings_patch(command_id, patch_json, now)?;
val settings: Flow<AccountSettings> = account.settings.observe()
account.settings.apply(
CommandId.random(),
AccountSettingsPatch(
notificationContentPrivacy =
SettingPatch.Set(NotificationContentPrivacy.SENDER),
imageUploadQuality = SettingPatch.Set(UploadQuality.HIGH),
typingIndicatorsEnabled = SettingPatch.Set(false),
),
)
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
account.relaysOwn 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
- Rust
- Android
AccountRuntimeHandle::relay_catalogAccountRuntimeHandle::add_account_relayAccountRuntimeHandle::set_account_active_relayAccountRelays.observeAccountRelays.addAccountRelays.setActive- Rust
- Android
let catalog = account.relay_catalog()?;
account.add_account_relay("wss://relay.example".into(), now)?;
account.set_account_active_relay("wss://relay.example".into(), now)?;
val relays: Flow<List<RelayCatalogEntry>> = account.relays.observe()
account.relays.add("wss://relay.example")
account.relays.setActive("wss://relay.example")
account.relays.exclude("wss://old-relay.example")
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
account.route-incomingVerify 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
- Rust
- Android
route_nip59_envelopeNip59EnvelopeRouteSoftchat.routeIncomingEventIncomingRoute- Rust
- Android
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.
val route = Softchat.routeIncomingEvent(eventJson)
val reference = AccountReference(route.recipient.hex)
accountCoordinator.withAccount(reference) { account ->
val committed = account.incoming.ingestJson(listOf(eventJson))
renderNotifications(committed.changes)
}
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
account.transportDrive 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
- Rust
- Android
AccountRuntimeHandle::start_account_transportAccountRuntimeHandle::apply_account_transport_resultAccountRuntimeHandle::reset_account_syncAccountTransport.startAccountTransport.applyAccountSynchronization.resetAccountIncoming.ingestJson- Rust
- Android
let batch = account.start_account_transport(run_id)?;
let next = account.apply_account_transport_result(result, now)?;
let reset = account.reset_account_sync(now)?;
var batch = account.transport.start(UUID.randomUUID().toString())
for (action in batch.actions) {
val result = executeWithKtor(action)
batch = account.transport.apply(result)
}
// A lifecycle or external-revision wake is only a hint.
account.refresh()
account.transport.wake()
// Keep authenticated history; rebuild synchronization checkpoints.
account.sync.reset()
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
account.mediaPersist 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
- Rust
- Android
AccountRuntimeHandle::prepare_media_messageAccountRuntimeHandle::prepare_media_downloadAccountRuntimeHandle::claim_media_operationAccountMedia.prepareMessageAccountMedia.prepareDownloadAccountMedia.claim- Rust
- Android
let pending = account.prepare_media_message(
command_id,
conversation_id,
content,
String::new(),
sources,
now,
)?;
let operations = account.media_operations(50)?;
val staged = androidMedia.stage(selectedUris)
val pending = account.media.prepareMessage(
commandId = CommandId.random(),
conversationId = conversation.id,
content = MessageContent(text = caption),
sources = staged.map { MediaPreparation(it.sourceFingerprint) },
)
androidMedia.bindAndSchedule(account, pending, staged)
// Download metadata is resolved from an authenticated stored message.
val download = account.media.prepareDownload(message.id, attachment.url)
check(download.sourceMessageId == message.id)
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
account.asset-replacementPersist 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
- Rust
- Android
AccountRuntimeHandle::prepare_asset_replacementAccountRuntimeHandle::pending_asset_replacementAccountRuntimeHandle::recover_pending_asset_replacementsAccountMedia.prepareProfilePictureAccountMedia.prepareProfileBannerAccountMedia.prepareConversationIconAccountMedia.pendingAssetReplacementAccountMedia.recoverReadyAssetReplacements- Rust
- Android
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)?;
// Copy the selected content URI into app-private staging first.
val staged = androidMedia.stage(selectedUri)
val commandId = CommandId.random() // persist/reuse while retrying this action
val pending = account.media.prepareProfilePicture(
commandId = commandId,
source = MediaPreparation(staged.sourceFingerprint),
)
check(pending.operation.protection == MediaProtection.PUBLIC)
// Bind the staged file to pending.operation.id, then enqueue WorkManager.
androidMedia.bindAndSchedule(account, pending, staged)
// The same pattern has typed entry points for the other supported targets.
account.media.prepareProfileBanner(commandId, MediaPreparation(fingerprint))
account.media.prepareConversationIcon(
commandId,
conversation.id,
MediaPreparation(fingerprint),
)
// On process start, after reopening the account:
account.media.recoverReadyAssetReplacements()
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
account.operationsInspect, 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
- Rust
- Android
AccountRuntimeHandle::operationAccountRuntimeHandle::operation_pageAccountRuntimeHandle::retry_operationAccountOperations.observeAccountOperations.retryAccountOperations.cancel- Rust
- Android
let operation = account.operation(command_id.clone())?;
let page = account.operation_page(50)?;
let retried = account.retry_operation(command_id)?;
val delivery: Flow<OperationSnapshot> =
account.operations.observe(commandId)
val pending = account.operations.observe(limit = 50)
account.operations.retry(commandId)
account.operations.cancel(commandId)
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
account.pushCreate 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
- Rust
- Android
AccountRuntimeHandle::register_push_tokenAccountPush.register- Rust
- Android
let registration = account.register_push_token(
command_id,
PushPlatform::Fcm,
token,
installation_id,
now,
)?;
account.push.register(
commandId = installationStore.commandForPushToken(fcmToken),
platform = PushPlatform.FCM,
token = fcmToken,
installationId = installationStore.id(),
)
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
account.contacts-resourcesStore 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
- Rust
- Android
AccountRuntimeHandle::put_local_contactAccountRuntimeHandle::replace_stickersAccountRuntimeHandle::put_link_previewAccountContacts.putAccountResources.replaceStickersAccountResources.putLinkPreview- Rust
- Android
account.put_local_contact(contact)?;
account.replace_stickers(stickers)?;
account.put_link_preview(preview)?;
account.contacts.put(
LocalContact(
publicKey = bobPublicKey,
name = "Bob",
updatedAtMillis = System.currentTimeMillis(),
),
)
val contacts = account.contacts.observe(limit = 100)
account.resources.replaceStickers(stickers)
account.resources.putLinkPreview(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
account.recoveryRecover 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
- Rust
- Android
AccountRuntimeHandle::recover_signed_outbox_batchAccountRecovery.importSignedOutboxBatch- Rust
- Android
let recovered = account.recover_signed_outbox_batch(
stable_legacy_row_id,
opaque_event_array_json,
now,
)?;
val recovered = account.recovery.importSignedOutboxBatch(
commandId = stableLegacyRowCommandId,
eventBatchJson = opaqueEventArrayJson,
)
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
account.supportObserve 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
- Rust
- Android
AccountRuntimeHandle::account_diagnosticsAccountSupport.observeDiagnosticsAccountSupport.observeRevisionSoftchatAccount.refresh- Rust
- Android
let diagnostics = account.account_diagnostics()?;
let revision = diagnostics.revision;
val diagnostics: Flow<AccountDiagnostics> =
account.support.observeDiagnostics()
val revisions: Flow<Long> = account.support.observeRevision()
// Use after a platform wake hint from another process or lifecycle owner.
account.refresh()
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
Flowqueries in Compose and retainCommandIdacross 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.