1. Home
  2. Case Studies
  3. Astra Chat
React NativeMatrix ProtocolEnd-to-End EncryptionLiveKitTypeScriptZustand4 WeeksSolo Lead Mobile Engineer

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
scroll

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.

Option A — Native Bridge

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.

Option B — Polyfill js-sdk

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.

Option C — react-native-matrix-sdk ✓ Chosen

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.

The catch with Option C

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

  1. Week 1

    Authentication & SDK Foundation

    Matrix SDK integration, login/registration flows, SSO via OIDC, session storage with Keychain, global state architecture with Zustand.

  2. Week 2

    Core Messaging System

    Real-time chat rooms, E2EE message flow, threads, replies, reactions, typing indicators, contact management and user directory.

  3. Week 3

    Calls, Media & Notifications

    LiveKit integration, 1:1 and group calling, native call UI, encrypted media upload/download, E2EE push notification pipeline.

  4. 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

React Native CLI

Cross-platform iOS + Android, no Expo constraints

TypeScript

TypeScript

Strict typing throughout — critical for complex async flows

Zustand

Zustand

Lightweight state management, SDK-as-source-of-truth pattern

LiveKit

LiveKit

Self-hosted WebRTC infrastructure for voice/video

@unomed/react-native-matrix-sdk

@unomed/react-native-matrix-sdk

Matrix Rust SDK via UniFFI — full E2EE in mobile

React Native Keychain

React Native Keychain

OS-level secure session storage, no backend needed

@notifee/react-native

@notifee/react-native

E2EE-safe push notifications without Firebase

R

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

Building a Matrix or E2EE mobile product?

I ship React Native products end to end — protocol integration, encryption, calls, and App Store release. Remote-friendly.