Skip to content

React Native / Expo

Run the whole stack on React Native (Hermes) — actions and realms both — with two standard polyfills the connection layer needs.

Everything in nice-code runs on React Native. The libraries use only standard web interfaces — never browser-only APIs — so the same code that runs in the browser, Node, and Cloudflare Workers runs on Hermes too. Because the connection layer is shared, unblocking it unblocks both planes: typed actions and live realms work identically on-device.

Hermes is missing two standard interfaces the secure connection relies on. You polyfill them once — there’s nothing nice-code-specific to install.

  • WebCrypto (crypto.subtle) — the handshake and encryption need Ed25519 / X25519 / HKDF / AES-GCM.
  • URL — Hermes’ built-in URL is incomplete; carriers use it to build WebSocket / HTTP urls.

Install the two polyfill packages:

Terminal window
bun add react-native-quick-crypto react-native-url-polyfill

Run them as the very first side-effect of your app entry — before any connection, ActionRuntime, or realm is constructed (so global.crypto and URL already exist by the time the library touches them):

polyfills.ts
// `URL` is incomplete in Hermes — fix it first.
import "react-native-url-polyfill/auto";
import { install } from "react-native-quick-crypto";
// Patches `global.crypto` with OpenSSL-backed WebCrypto (SubtleCrypto).
install();
index.js
// Import the polyfills before anything else (e.g. before "expo-router/entry").
import "./polyfills";
import "expo-router/entry";

react-native-quick-crypto compiles OpenSSL through Nitro / JSI, so it needs the new architecture enabled and a recent deployment target (iOS 16.4+, Android minSdkVersion 24+). Configure that via expo-build-properties if you’re on Expo.

That’s the whole requirement. The crypto identity, secure WebSocket, HTTP exchange, and realms all work from there exactly as they do on every other platform.

The crypto identity is persisted through a StorageAdapter — and on a secure connection this store must be durable, or the pinned identity is rejected after the first launch (see Identity & Trust). On the web that’s localStorage; on React Native, back it with @react-native-async-storage/async-storage:

async_storage.ts
import { EStorageAdapterType, type IStorageAdapterMethods_String } from "@nice-code/util";
import AsyncStorage from "@react-native-async-storage/async-storage";
export const asyncStorageMethods: IStorageAdapterMethods_String = {
type: EStorageAdapterType.string,
setItem: (key, value) => AsyncStorage.setItem(key, value),
getItem: (key) => AsyncStorage.getItem(key),
removeItem: (key) => AsyncStorage.removeItem(key),
};

Wrap it in a StorageAdapter and pass it as the storage on createWireClient (realm-only) or connectChannel (with actions) — the same way the web demo passes a localStorage-backed adapter.