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.
| Concern | Softchat owns | Your application owns |
|---|---|---|
| Identity | validation, public-key derivation, signing, NIP-44 | secure storage, unlock policy, authority acquisition |
| Events | canonical IDs, signatures, raw tags, typed projections | database schema, indexing, retention |
| Relay | frame codecs, state transitions, action plans | WebSocket, timers, connectivity, retry scheduling |
| Persistence | bounded ingestion batches and result validation | transactions, migrations, durable storage |
| Noise | exact handshake/transport state and chunking | socket bytes, connection lifecycle |
| Sync | bounded Negentropy snapshots and reconciliation | querying and changing the database |
| Attachments | metadata validation, streaming transform, NIP-98 plan | files, staging, HTTP, redirects, background transfer |
| Diagnostics | stable counters, redacted failures, build fingerprint | telemetry transport, sampling, user consent |
Trust transitionsโ
- Validate bytes or JSON before treating them as a protocol value.
- Authenticate every NIP-59 layer before exposing its rumor.
- Persist an inbound relay event in one host transaction.
- Call
confirmIngestedonly after that transaction commits. - Stage decrypted media privately until
finishauthenticates 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.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
LocalIdentity, RelaySession, NoiseTransport, and stream objects own
state. Use typed newtypes for keys and event identifiers at trust boundaries.
Keep stateful objects in an account or connection scope. RelaySession
implements AutoCloseable; SoftchatIdentity deliberately does not.
Use Room, WorkManager, Android networking, and app lifecycle components as adapters around the same Kotlin public API. Never retain generated JNA objects in ViewModels or persistence.
One actor should serialize a relay connection, timers, and its
RelaySession/NoiseTransport. Keep URLSession, GRDB, files, and background
tasks outside SoftchatKit.
Use a Worker for large synchronous operations. Keep WebSocket, fetch,
IndexedDB, service workers, and asset resolution in JavaScript.
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.