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

Building a Decentralized, End-to-End Encrypted Chat App in React Native — Without the Official SDK

Engineered a production-grade, Matrix-protocol messaging app for iOS and Android — solving a fundamental SDK incompatibility, architecting a zero-backend encryption system, and shipping within a 4-week deadline.

4 wks
Full delivery
0
Third-party backends
iOS+Android
Cross-platform
E2EE
Full encryption
scroll

01 — Situation

The Client and Their Vision

Astra is building a compliance-first digital trust platform — a decentralized ecosystem where businesses and partners communicate securely without relying on centralized services like Firebase or big-tech auth providers. The vision was a messaging super-app that could stand alongside Telegram and Slack in UX, while running on infrastructure the client actually controlled.

The client had an existing iOS app (ChitchatX) and needed it rebuilt as a cross-platform React Native product. The core protocol was already chosen: Matrix — an open, federated, decentralized communication standard with built-in E2EE. My job was to make it work.

Constraints

React Native CLI · No Firebase · No Google/Facebook auth · Matrix-native flows only · 4-week delivery window · iOS + Android parity · White-label ready architecture

02 — The Problem

The Matrix SDK Wall

Matrix provides an official JavaScript SDK (matrix-js-sdk). The obvious path would be to install it and start building. But there was a fundamental incompatibility that stopped this cold.

The Blocker

The official SDK depends on browser-only APIs — WebAssembly, the Web Crypto API, and IndexedDB — for its E2EE layer. React Native has none of these. Encryption breaks entirely on mobile.

What This Meant

The primary SDK Matrix officially supports was completely off the table. Every alternative came with its own painful trade-offs around timeline, maintainability, and team expertise.

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. The architectural soundness — using the official Rust SDK rather than a polyfill or from-scratch client — gave it the highest long-term reliability. The lack of documentation was a known cost, not a dealbreaker. I had a plan to address it.

03 — Thinking

Turning an Undocumented SDK into a Production Foundation

The biggest risk in choosing an undocumented SDK isn't the SDK itself — it's discovery time. Without knowing what it could and couldn't do, I could easily spend a week chasing a feature that simply wasn't implemented. I needed a way to make it legible, fast.

Using AI to generate SDK documentation

The SDK generates binding files through UniFFI — large, structured FFI interface files that describe every available method, type, and event emitter. I fed these into Google Gemini and prompted it to produce structured documentation. This gave me a working reference in hours rather than days, and let me plan the full implementation before writing a line of app code.

Separating the Matrix client from the UI layer

One architectural decision I made early was to never let the Matrix client bleed into UI components. The SDK is event-driven and stateful — treating it like a React state source would cause cascading re-renders and race conditions. Instead, I built TypeScript classes to encapsulate all Matrix operations, with Zustand as a one-way bridge: SDK events update the store, components read from the store.

// 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)
    })
  }
}

Securing sessions without a backend

With no Firebase and no custom backend, session persistence lived entirely on-device. I used react-native-keychain to store Matrix credentials in the OS-level secure enclave (Keychain on iOS, Keystore on Android). The SDK handles key synchronization with the homeserver automatically.

Notifications without Firebase

Matrix's notification model sends an event ID (not the message content) to the push system — this preserves E2EE. I implemented a background fetch handler using @notifee/react-native: when a push arrives, the app fetches event details from the homeserver, decrypts locally, and displays the notification. The push infrastructure sees only an opaque event ID.

04 — Action

What I Built

I led the project as sole senior engineer — responsible for all architecture decisions, the full Matrix integration, calling infrastructure, and encryption flows. A junior developer joined mid-project for UI components, which I then wired into 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

Calling architecture

For voice and video, I used a self-hosted LiveKit instance, signalled through Matrix room events. When a user initiates a call, the app sends a Matrix event to the room containing LiveKit connection parameters. Matrix handles signalling; LiveKit handles media. Full infrastructure control, no third-party calling services.

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 — App Screenshots

The Finished Product

11 screens from the shipped app — click any to enlarge. Use the arrows or dots to browse.

1 / 11Splash Screen · Click any screen to enlarge


07 — Reflection

What This Project Proved

The hardest part wasn't the code — it was making the right call under uncertainty. Choosing an undocumented, community-maintained SDK over the "official" path required weighing technical risk against timeline risk, and trusting that a sound architectural foundation (the Rust SDK) would hold. It did.

The approach of using AI to generate documentation from raw FFI files is something I'd use again without hesitation. It turned an opaque black box into a navigable API reference in a fraction of the time manual exploration would have taken.

If I were doing this again, I'd invest more time in automated integration tests against the Matrix homeserver earlier — several late-week fixes were harder to trace than they needed to be.

"This project taught me that the most valuable skill isn't knowing the answer — it's building a fast, reliable path to the answer when no one has documented it yet."

Muhammad Hassan, personal reflection