Skip to main content

Architecture and boundaries

The integration rule is simple: Rust owns deterministic protocol semantics; the host owns effects. A platform facade converts between idiomatic values and the core without implementing a second cryptographic or protocol path.

ConcernSoftchat ownsYour application owns
Identityvalidation, public-key derivation, signing, NIP-44secure storage, unlock policy, authority acquisition
Eventscanonical IDs, signatures, raw tags, typed projectionsnon-Android storage policy and retention
Relay sessionframe codecs, state transitions, action plansWebSocket execution when using the low-level API
Managed native relayWSS/TLS/Noise, timeouts, reconnect timing, action correlation, shutdownplatform trust initialization, reachability hints, account lifecycle
Android persistenceSQLite schema, migrations, transactions, projections, delivery recovery, queriesapp-private path, account lifecycle, IO dispatch
Other persistencebounded ingestion contracts and validationtransactions, migrations, durable storage
Noiseexact handshake/transport state and chunkingcustom socket bytes, or the managed native relay
Android syncfingerprint queries, reconciliation, ingestion, checkpointmanaged relay lifecycle or custom WebSocket execution
Other syncbounded Negentropy snapshots and reconciliationquerying and changing the database
Attachmentsmetadata validation, streaming transform, NIP-98 planfiles, staging, HTTP, redirects, background transfer
Diagnosticsstable counters, redacted failures, build fingerprinttelemetry transport, sampling, user consent

Trust transitionsโ€‹

  1. Validate bytes or JSON before treating them as a protocol value.
  2. Authenticate every NIP-59 layer before exposing its rumor.
  3. Persist an inbound relay event in one authoritative transaction; Android calls SoftchatAccount.incoming.ingest.
  4. Call confirmIngested only after that transaction commits.
  5. Stage decrypted media privately until finish authenticates its tag.

Receiving bytes is not persistence. Sending bytes is not delivery. A WebSocket callback must therefore never mark an event as safely stored or acknowledged without the corresponding state transition.

Data ownershipโ€‹

  • Identities, Noise transports, Negentropy clients, relay sessions, and attachment streams hold native state.
  • Events, rumors, filters, projections, request plans, diagnostics, and encrypted messages are ordinary immutable language-owned values.
  • Generated UniFFI/JNA, UniFFI Swift/C, and raw wasm-bindgen APIs are internal and may change without notice.

LocalIdentity, RelaySession, NoiseTransport, and stream objects own state. Use typed newtypes for keys and event identifiers at trust boundaries.

Bounds and failure policyโ€‹

Inputs are bounded before expensive work. The portable signed-event size limit is 512 KiB; timestamp, tag, batch, frame, Noise, reconciliation, plaintext, ciphertext, and attachment limits are enforced by the relevant constructor or operation. Treat a rejected limit as a permanent input failure, not a reason to retry with more memory.

Errors are stable and redacted. Never branch on display text or upstream exception names. See errors and diagnostics.