| summary | Rust Multiplatform model — Rust-owned state/logic plus native capability bridges | |||
|---|---|---|---|---|
| read_when |
|
This repo follows a Rust Multiplatform architecture:
- Rust is the source of truth for business logic and state.
- Native apps (iOS/Android) are primarily functional renderers of Rust state.
- We prefer Rust-first implementations by default.
- Native code may host narrowly scoped platform adapters when they are required to deliver true first-class platform UX.
See also:
docs/architecture.mdfor topologydocs/state.mdforAppState/AppUpdateflowdocs/rmp-ci.mdfor CI lanes
RMP must never produce a second-class experience versus a true native app.
- If a Rust-only approach can achieve native-quality behavior, prefer Rust.
- If platform-native APIs are required to match native UX/quality, use a native capability bridge.
- "Cross-platform purity" does not override user experience quality.
Rust owns:
- state machines and policy decisions
- protocol/transport/crypto behavior
- long-lived application state (
AppState+ actor-internal derivation state) - cross-platform invariants and error semantics
Native owns:
- rendering and UX affordances
- platform capability execution where native APIs are required to achieve first-class native behavior (system audio routing, push surfaces, URL handoff, etc)
- short-lived handles to OS resources
Native must not own app business logic.
- Prefer Rust tests that dispatch frontend-like actions and assert on Rust-owned state.
- Prefer local fixture-backed integration coverage over public-network or deployed-bot probes.
- Keep native tests for rendering behavior and true platform capability bridges rather than re-testing Rust-owned business logic.
(adapter window is an acceptable alias during transition.)
When we need platform APIs, we use a native capability bridge.
A native capability bridge is a bounded lifecycle where Rust leases a single responsibility to native runtime code while keeping policy/state ownership in Rust.
- Rust opens window by command:
- Example: "start call audio IO", "open external signer URL", "arm push decrypt context".
- Native executes platform side effect:
- OS callbacks, device/session setup, route handling, permission-coupled operations.
- Native reports events/data back to Rust:
- typed callbacks/events only (no native-owned policy transitions).
- Rust decides:
- state updates, retries, fallbacks, user-visible outcomes, telemetry semantics.
- Rust closes window:
- deterministic teardown command and native resource release.
- Rust-defined API: callbacks and events are versioned and owned by Rust contracts.
- No policy forks: native cannot introduce alternate call/login/message state machines.
- Bounded native state: keep only transient buffers/handles required by OS callbacks.
- Idempotent lifecycle: start/stop/restart paths must be safe during interruptions.
- Observable boundary: emit counters/errors so Rust can compare behavior and decide rollouts.
Put in Rust when it is:
- policy, branching behavior, retries, and fallback decisions
- protocol semantics and validation
- state that affects routing, UX flow, or correctness
- logic shared across iOS/Android/desktop
Put in native adapter when it is:
- hard OS integration point (real-time audio callback graphs, push extension execution, app-switch URL intents)
- route/session/device APIs unavailable or unreliable via cross-platform abstraction
- strict platform UX behavior (system call UI expectations, interruption handling)
Preference order:
- Rust implementation that preserves native-quality UX.
- Native capability bridge if and only if required for native-quality UX.
- External signer bridge:
- Rust trait boundary in
rust/src/external_signer.rs - native implementations in app layers
- Rust still owns login flow state machine and error policy
- Rust trait boundary in
- Push/NSE split:
- Rust crypto/decrypt logic in
crates/pika-nse - iOS Notification Service Extension hosts Apple notification lifecycle
- Rust crypto/decrypt logic in
- Call audio migration (planned):
- native owns audio callback graph and voice-processing primitives
- Rust keeps call state machine, media transport, crypto, jitter/codec policy
Before adding native logic, answer:
- Can a Rust-first implementation match true native UX/quality on target platforms?
- Can we isolate it behind a narrow Rust-owned contract?
- Is native state purely transient/operational?
- Does Rust still decide policy and user-visible outcomes?
- Do we have telemetry to validate and rollback?
If any answer is "no", keep moving logic back into Rust until it is.