End-to-end encrypted messaging for iOS and Android, built on MLS over Nostr.
Warning
Alpha software. This project was largely vibe-coded and likely contains privacy and security flaws. Do not use it for sensitive or production workloads.
| Feature | iOS | Android | Desktop |
|---|---|---|---|
| 1:1 encrypted messaging | ✅ | ✅ | ✅ |
| Group chats (MLS) | ✅ | ✅ | ✅ |
| Voice calls (1:1) | ✅ | ✅ | ✅ |
| Video calls (1:1) | ✅ | ✅ | |
| Push notifications | ✅ | ||
| Emoji reactions | ✅ | ✅ | |
| Typing indicators | ✅ | ✅ | |
| @mention autocomplete | ✅ | ✅ | |
| Markdown rendering | ✅ | ✅ | |
| Polls | ✅ | ✅ | |
| Interactive widgets (HTML) | ✅ | ||
| QR code scan / display | ✅ | ✅ | |
| Encrypted media upload/download | ✅ | ||
| Profile photo upload | ✅ | ✅ | |
| Follow / unfollow contacts | ✅ | ✅ | ✅ |
Pika uses the Marmot protocol to layer MLS group encryption on top of Nostr relays. Messages are encrypted client-side using MLS, then published as Nostr events. Nostr relays handle transport and delivery without ever seeing plaintext.
┌─────────┐ UniFFI / JNI ┌────────────┐ Nostr events ┌───────────┐
│ iOS / │ ─── actions ────────▶ │ Rust core │ ── encrypted msgs ──▶ │ Nostr │
│ Android │ ◀── state snapshots ── │ (pika_core)│ ◀─ encrypted msgs ── │ relays │
└─────────┘ └────────────┘ └───────────┘
│
▼
┌────────────┐
│ MDK │
│ (MLS lib) │
└────────────┘
- Rust core owns all business logic: MLS state, message encryption/decryption, Nostr transport, and app state
- iOS (SwiftUI) and Android (Kotlin) are thin UI layers that render state snapshots from Rust and dispatch user actions back
- MDK (Marmot Development Kit) provides the MLS implementation
- nostr-sdk handles relay connections and event publishing/subscribing
pika/
├── rust/ Rust core library (pika_core) — MLS, Nostr, app state
├── ios/ iOS app (SwiftUI, XcodeGen)
├── android/ Android app (Kotlin, Gradle)
├── cli/ pikachat — command-line tool for testing and automation
├── crates/
│ ├── pikachat-sidecar/ Pikachat daemon engine (shared library)
│ ├── pika-media/ Media handling (audio, etc.)
│ ├── pika-tls/ TLS / certificate utilities
│ └── rmp-cli/ RMP scaffolding CLI
├── uniffi-bindgen/ UniFFI binding generator
├── docs/ Architecture and design docs
├── tools/ Build and run tooling (pika-run, etc.)
├── scripts/ Developer scripts
└── justfile Task runner recipes
- Rust (stable toolchain with cross-compilation targets)
- Nix (optional) —
nix developprovides a complete dev environment - iOS: Xcode, XcodeGen
- Android: Android SDK, NDK
The Nix flake (flake.nix) pins all dependencies including Rust toolchains and Android SDK components. This is the recommended way to get a reproducible environment.
just rust-build-hostjust ios-rust # Cross-compile Rust for iOS targets
just ios-xcframework # Build PikaCore.xcframework
just ios-xcodeproj # Generate Xcode project
just ios-build-sim # Build for simulator
just run-ios # Build, install, and launch on simulatorjust android-local-properties # Write local.properties with SDK path
just android-rust # Cross-compile Rust for Android targets
just gen-kotlin # Generate Kotlin bindings via UniFFI
just android-assemble # Build debug APK
just run-android # Build, install, and launch on device/emulatorA command-line interface for testing the Marmot protocol directly:
just cli-build
cargo run -p pikachat -- --relay ws://127.0.0.1:7777 identity
cargo run -p pikachat -- --relay ws://127.0.0.1:7777 groupsjust fmt # Format Rust code
just clippy # Lint
just test # Run pika_core tests
just qa # Full QA: fmt + clippy + test + platform builds
just pre-merge # CI entrypoint for the whole repoSee all available recipes with just --list.
just test # Unit tests
just cli-smoke # CLI smoke test (requires local Nostr relay)
just e2e-local-relay # Deterministic E2E with local relay + local bot
just e2e-public # E2E against public relays (nondeterministic)
just ios-ui-test # iOS UI tests on simulator
just android-ui-test # Android instrumentation testsPika follows a unidirectional data flow pattern:
- UI dispatches an
AppActionto Rust (fire-and-forget, never blocks) - Rust mutates state in a single-threaded actor (
AppCore) - Rust emits an
AppUpdatewith a monotonic revision number - iOS/Android applies the update on the main thread and re-renders
State is transferred as full snapshots over UniFFI (Swift) and JNI (Kotlin). This keeps the system simple and eliminates partial-state consistency bugs.
See docs/architecture.md for the full design.