| summary | High-level architecture — Rust core, iOS/Android apps, MLS over Nostr | ||
|---|---|---|---|
| read_when |
|
Pika is an MLS-encrypted messaging app for iOS and Android, built on the Marmot protocol over Nostr.
For the canonical Rust Multiplatform model (including native adapter windows), see docs/rmp.md.
This page is the topology overview.
- Rust core (
rust/) — MLS state machine, Nostr transport, UniFFI bindings - Call control (Rust core) — call signaling state machine over MLS app messages (
pika.callnamespace) - iOS app (
ios/) — Swift UI, uses PikaCore.xcframework - Android app (
android/) — Kotlin, uses JNI bindings via cargo-ndk - pikachat (
cli/) — Command-line interface for testing and agent automation - MDK (external,
https://github.com/marmot-protocol/mdk) — Marmot Development Kit, the MLS library
- App calls Rust core via UniFFI (Swift) or JNI (Kotlin)
- Rust core uses MDK for MLS group operations (create, invite, encrypt, decrypt)
- Rust core uses nostr-sdk to publish/subscribe Nostr events on relays
- Key packages (kind 443) enable async peer discovery
Goal: Rust owns core app state + business logic. iOS/Android mostly render Rust-owned state slices and forward user actions.
iOS/Android may keep UI-only ephemeral state (text inputs, focus, scroll position, local toggles). iOS/Android should not own core state that affects app behavior or routing (for example, "loading"/in-flight state for a login/create-chat flow should be Rust-owned).
- UI dispatches an
AppActionto Rust (dispatch(action)is enqueue-only and must not block the UI thread) - Rust mutates its internal state in a single-threaded actor (
AppCore) - Rust emits an
AppUpdatewith a monotonicrev - iOS/Android applies updates on the main thread and re-renders
- If iOS/Android detects a
revgap, it resyncs viastate()
AppState.router encodes navigation:
default_screen: root (e.g.LoginvsChatList)screen_stack: pushed screens (e.g.Chat { chat_id })
Everything needed to render a screen lives in AppState slices, not in iOS/Android-owned core state:
chat_list: chat list screencurrent_chat: chat screen render model (messages, title, delivery status)toast: transient user-visible messages
Invariant: when the top route is Screen::Chat { chat_id }, Rust keeps current_chat populated for that chat_id.
iOS/Android never "fetches" chat data; it only renders AppState.
Long-ish operation state that affects UX lives in Rust as AppState.busy (e.g. creating_chat, logging_in).
This avoids iOS/Android heuristics like "stop spinner when a toast appears" and keeps UI purely reactive to Rust state.
Testing follows the same boundary: prefer Rust-level action/state assertions and local fixtures for business logic, and reserve native tests for renderer behavior or true platform-bridge concerns.
docs/rmp.md— Rust Multiplatform ownership model + adapter window patterndocs/state.md—AppState/AppUpdatedetails