- Home
- Case Studies
- Astra Chat
React Native Matrix case study: E2EE chat shipped in 4 weeks
I built a production Matrix messaging app for iOS and Android — no matrix-js-sdk, no Firebase, no custom backend. Solo lead mobile engineer. Full E2EE, LiveKit calls, and encrypted push.
- Full delivery
- 4 wks
- Third-party backends
- 0
- Cross-platform
- iOS+Android
- Full encryption
- E2EE
01 — Situation
Why Matrix, and why rebuild in React Native
Astra is building a compliance-first digital trust platform. Businesses need secure messaging without Firebase, Google Auth, or other centralized backends they do not control.
The client had an iOS app (ChitchatX) and needed a cross-platform rebuild. The protocol was already chosen: Matrix — open, federated, and E2EE-capable by default. My job was to make it work on React Native.
Constraints
React Native CLI (not Expo) · No Firebase or big-tech auth · Matrix-native flows only · 4-week delivery window · iOS + Android parity · White-label-ready architecture
02 — The Problem
The matrix-js-sdk wall on React Native
Matrix ships matrix-js-sdk for JavaScript clients. On paper, install it and build. On React Native, that path breaks at the crypto layer.
The Blocker
The official SDK depends on browser APIs — WebAssembly, Web Crypto, and IndexedDB — for E2EE. React Native has none of these. Encryption fails on device.
What This Meant
The SDK Matrix documents for web clients was off the table. Every alternative carried timeline, maintenance, or correctness risk.
Use Matrix's official Rust/Kotlin/Swift SDKs natively and bridge to RN via NativeModules. Full E2EE support — but requires building everything twice (iOS + Android separately), massively expanding scope and timeline.
Polyfill the missing browser APIs in React Native. Fragile — crypto polyfills are incomplete, IndexedDB shims unreliable under load, and encryption correctness becomes impossible to guarantee.
A third-party SDK (@unomed/react-native-matrix-sdk) that uses the official Matrix Rust SDK under the hood, generating RN Turbo Modules via UniFFI. Not officially supported — but architecturally sound.
Zero documentation. The SDK generates bindings from the Rust SDK — meaning actual React Native implementation patterns had to be reverse-engineered entirely from the generated FFI interface files.
Decision
I chose Option C — @unomed/react-native-matrix-sdk. It uses the official Rust SDK via UniFFI, not browser polyfills. Missing docs were a known cost. I had a plan: generate API reference from FFI bindings before writing app code.
03 — Architecture
How I made an undocumented SDK shippable
I chose @unomed/react-native-matrix-sdk — Rust Matrix SDK bindings via UniFFI Turbo Modules. The risk was not the SDK. It was zero documentation.
Agentic doc generation from UniFFI bindings
The SDK exposes large FFI interface files — every method, type, and event. I fed those into Gemini and generated structured API docs in hours. That let me plan the full app before writing UI code.
Matrix client isolated from React UI
The Matrix client is event-driven and stateful. It cannot drive React renders directly. I wrapped operations in TypeScript classes and used Zustand as a one-way bridge: SDK events update the store, components read the store.
Sessions without a backend
With no Firebase and no custom API, credentials lived on-device. react-native-keychain stores Matrix sessions in the OS secure enclave. The SDK syncs keys with the homeserver.
E2EE push without Firebase
Matrix push sends an event ID, not message text — E2EE stays intact. @notifee/react-native runs a background handler: fetch the event, decrypt locally, then show the notification. Push infra only sees opaque IDs.
// Architecture pattern: Matrix client is a singleton class
// that consumes SDK events and pushes to Zustand store
class MatrixChatClient {
private client: MatrixClient | null = null
async initialize(session: StoredSession) {
this.client = await MatrixClient.create({
homeserverUrl: session.homeserver,
userId: session.userId,
deviceId: session.deviceId,
})
// SDK is the source of truth — Zustand only mirrors it
this.client.on('room.timeline', (event) => {
useChatStore.getState().addMessage(event)
})
}
}04 — Delivery
What I built and owned
I was the sole senior mobile engineer — architecture, Matrix integration, encryption, calling, and release. A junior engineer joined mid-project for UI screens. I wired every screen to the Matrix data layer.
- ✓Matrix auth (login, registration, SSO/OIDC)
- ✓End-to-end encrypted 1:1 and group messaging
- ✓Real-time updates via event-driven SDK
- ✓Message threads, replies, reactions
- ✓Typing indicators and read receipts
- ✓Encrypted file and media sharing
- ✓1:1 and group VoIP calls via LiveKit
- ✓Native call UI (CallKit / Android)
- ✓E2EE push notifications (no Firebase)
- ✓Device verification (QR + SAS cross-signing)
- ✓Secure key backup and session recovery
- ✓SQLite caching via the Matrix SDK
- ✓Secure session storage via OS Keychain
- ✓White-label ready component architecture
LiveKit + Matrix signalling
Voice and video use a self-hosted LiveKit instance. Call setup travels as Matrix room events with connection params. Matrix handles signalling. LiveKit handles media. No third-party calling SaaS.
4-week delivery breakdown
Week 1
Authentication & SDK Foundation
Matrix SDK integration, login/registration flows, SSO via OIDC, session storage with Keychain, global state architecture with Zustand.
Week 2
Core Messaging System
Real-time chat rooms, E2EE message flow, threads, replies, reactions, typing indicators, contact management and user directory.
Week 3
Calls, Media & Notifications
LiveKit integration, 1:1 and group calling, native call UI, encrypted media upload/download, E2EE push notification pipeline.
Week 4
Security, Polish & Delivery
Device verification, key backup, cross-signing, settings, global search, bug fixes, performance profiling and production build.
05 — Stack
Technology Choices
React Native CLI
Cross-platform iOS + Android, no Expo constraints
TypeScript
Strict typing throughout — critical for complex async flows
Zustand
Lightweight state management, SDK-as-source-of-truth pattern
LiveKit
Self-hosted WebRTC infrastructure for voice/video
@unomed/react-native-matrix-sdk
Matrix Rust SDK via UniFFI — full E2EE in mobile
React Native Keychain
OS-level secure session storage, no backend needed
@notifee/react-native
E2EE-safe push notifications without Firebase
react-native-callkeep
Native call UI integration (CallKit + Android)
06 — Product
Shipped UI — 11 production screens
Screens from the released app. Tap to enlarge. Arrows and dots browse the set.
1 / 11 — Splash Screen · Click any screen to enlarge
07 — Reflection
What this case study proves
The hard part was not typing code. It was choosing an undocumented community SDK over the official path — under a 4-week deadline — and trusting the Rust foundation would hold. It did.
Generating docs from UniFFI bindings with AI is a workflow I would use again. It turned an opaque SDK into a navigable API reference in hours, not days of manual spelunking.
If I rebuilt this today, I would add homeserver integration tests in week one. Several late-week bugs were harder to trace than they needed to be.
"The valuable skill is not memorizing an SDK. It is building a fast, reliable path to answers when nobody has documented them yet."
— Muhammad Hassan, personal reflection