Skip to Content
SDK IntegrationReact Native (iOS & Android)

React Native SDK

Integrate TrustPin into your React Native application for certificate pinning on iOS and Android.

Current version: @trustpin/react-native 6.4.0

Using an AI coding agent? Install the TrustPin skill and let it wire this up for you.

Pinning is configured and activated in native code, before any JavaScript runs, so it cannot be weakened from JS, including from over-the-air JS updates. The JavaScript API is observe-only: it reports readiness and events but holds no lever that disables or reconfigures pinning.

Platform Requirements

PlatformMinimum Version
iOS15.0+
AndroidAPI 25+ (Android 7.1)
React Native0.85+ (New Architecture only)
Node22.11+

Additional Requirements:

  • iOS: added to the app target’s Copy Bundle Resources phase
  • Android: Kotlin 2.3.0+, minSdk 25 (RN’s default is 24)

No macOS support. This SDK targets iOS and Android only. macOS support will follow once react-native-macos reaches the RN 0.85 line.


Installation

npm install @trustpin/react-native # or yarn add @trustpin/react-native

Then follow the setup for your project type below (Expo or bare React Native).


Quick Start

1. Get Your Credentials

Sign in to the TrustPin Dashboard  and retrieve:

  • Organization ID
  • Project ID
  • Public Key (Base64-encoded)

Credentials are not secret, but they identify your project. Keep them out of public source control.

2. Set Up TrustPin

TrustPin is bootstrapped in native code before the JavaScript runtime does any networking. The setup differs for Expo and bare React Native projects.

Expo

Add the config plugin to app.json / app.config.js with your credentials:

{ "expo": { "plugins": [ ["@trustpin/react-native", { "organizationId": "your-org-id", "projectId": "your-project-id", "publicKey": "LS0tLS1CRUdJTi...", "mode": "strict" }] ] } }

Then generate the native projects and run:

npx expo prebuild npx expo run:ios # or: npx expo run:android

The plugin writes the native config files, wires the native init call on both platforms, and applies the Android toolchain requirements (Kotlin 2.3.0, minSdk 25). Expo Go cannot run pinning, it is native code, so use a development build. Prefer app.config.js with environment variables to keep credentials out of public source.

The config plugin accepts these props:

PropTypeNotes
organizationIdStringRequired unless using configFile
projectIdStringRequired unless using configFile
publicKeyStringBase64 verification key. Required unless using configFile
modestrict | permissiveDefaults to strict
configurationUrlStringOptional. HTTPS endpoint for a self-hosted signed config. Must point at a public host. See Custom configuration URLs
embeddedConfigurationFileStringOptional. Path to a signed configuration shipped as a last-resort fallback. See Embedded Configuration
logLevelnone | error | info | debugPassed to the native init helper, so it also covers startup logging
ios.configFileStringPath to an existing TrustPin-Info.plist instead of generating one
android.configFileStringPath to an existing trustpin.json instead of generating one
android.allowNonOemImagesBooleanDefault false. Allows release builds on non-OEM device OS images (real devices only, not emulators)

Inline credentials and a configFile for the same platform are rejected rather than silently resolved, and partial credentials are rejected too, naming what is missing.

Bare React Native

Bare apps ship the native config files and add one native init call per platform.

Step 1: Ship the configuration files

iOS: add ios/<YourApp>/TrustPin-Info.plist and add it to the app target’s Copy Bundle Resources phase in Xcode:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>OrganizationId</key> <string>your-org-id</string> <key>ProjectId</key> <string>your-project-id</string> <key>PublicKey</key> <string>LS0tLS1CRUdJTi...</string> <key>Mode</key> <string>strict</string> </dict> </plist>
KeyTypeRequiredNotes
OrganizationIdStringYesNon-empty
ProjectIdStringYesNon-empty
PublicKeyStringYesBase64-encoded
ModeStringNo"strict" (default) or "permissive", lowercase
ConfigurationURLStringNoMust be HTTPS and point at a public host. See Custom configuration URLs
EmbeddedConfigurationFileStringNoResource filename of a bundled signed configuration. See Embedded Configuration

Android: add android/app/src/main/assets/trustpin.json (Gradle bundles it automatically):

{ "organization_id": "your-org-id", "project_id": "your-project-id", "public_key": "LS0tLS1CRUdJTi...", "mode": "strict", "configuration_url": "https://your-server.com/pins.jws" }

Step 2: Call the native init helper

iOS: in your native app bootstrap, such as AppDelegate.swift:

import TrustPinReactNative func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { TrustPinReactNative.start() // or: .start(logLevel: .debug) // ...existing React Native setup... }

Android: in your native app bootstrap, such as MainApplication.kt:

import cloud.trustpin.reactnative.TrustPinReactNative override fun onCreate() { TrustPinReactNative.start(this) // or: .start(this, TrustPinLogLevel.DEBUG) super.onCreate() loadReactNative(this) }

Step 3: Align the Android Kotlin toolchain

The native runtime requires an Android Kotlin toolchain version compatible with its shipped metadata. Align the Kotlin plugin version used by the app build with the runtime requirement in android/build.gradle:

buildscript { ext { kotlinVersion = "2.3.0" minSdkVersion = 25 // TrustPin requires 25; RN's default is 24 } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0") } }

Then cd ios && pod install, and rebuild the app.


Custom Configuration URLs

By default the SDK loads its signed configuration from the TrustPin CDN. The configurationUrl plugin prop, or the ConfigurationURL / configuration_url keys in the native config files, overrides that endpoint for self-hosted deployments.

New in 6.4.0: the URL must be HTTPS and point at a public host. Loopback and private addresses are now rejected by the native SDK while the configuration is validated, and startup fails with INVALID_PROJECT_CONFIG rather than falling back to the default endpoint.

RejectedExamples
Loopbacklocalhost, 127.0.0.1, ::1
Private ranges (RFC 1918)10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, which includes the Android emulator’s 10.0.2.2 host alias

If a debug build was pointing at a configuration server on your workstation, either serve the signed payload from a public HTTPS host or leave the option unset and use the hosted configuration.

This restriction applies only to where the signed configuration is fetched from. It does not affect which hosts your app can pin, and it does not affect requests to your own API.


Embedded Configuration

TrustPin fetches its signed pinning configuration online and keeps the last validated one on the device. For the one case where neither exists, the app’s very first start while every configuration source is unreachable, you can ship a signed configuration inside the app as a last-resort fallback.

Download the signed configuration for your project from the dashboard , then wire it up for your project type.

Expo: point the plugin at it. prebuild copies the file into both native projects and adds the matching key to the generated config files:

["@trustpin/react-native", { "organizationId": "your-org-id", "projectId": "your-project-id", "publicKey": "LS0tLS1CRUdJTi...", "embeddedConfigurationFile": "./trustpin-seed.b64" }]

embeddedConfigurationFile cannot be combined with ios.configFile or android.configFile: the plugin only adds the key to files it generates. With your own config files, declare the key yourself and ship the payload as shown below.

Bare React Native: ship the file on each platform and reference it by name.

  • iOS: add ios/<YourApp>/trustpin-seed.b64 to the app target’s Copy Bundle Resources phase, then add to TrustPin-Info.plist:
    <key>EmbeddedConfigurationFile</key> <string>trustpin-seed.b64</string>
  • Android: add android/app/src/main/assets/trustpin-seed.b64, then add to trustpin.json:
    "embedded_configuration_asset": "trustpin-seed.b64"

Requirements

  • Use it only in apps protected by RASP (runtime application self-protection) that guards bundled resources against modification. An unprotected app must not ship an embedded configuration.
  • The file must be the unmodified signed payload downloaded from the dashboard for this project. It is verified against publicKey during native setup, and a file that is missing, unreadable, or that fails verification fails startup with INVALID_PROJECT_CONFIG.
  • Regenerate it in CI on every release, so it is never older than the app that ships it. Pins expire on their own schedule, and an embedded configuration whose pins have all expired is equivalent to having no fallback. trustpin-cli projects jws writes the currently published payload to a file, so a release pipeline can refresh the bundled copy on every build.

Behaviour

  • It is never preferred over an online source, or over a configuration the SDK has already fetched and validated.
  • It is subject to the same integrity checks as any other configuration: a device that has already trusted a newer configuration will not accept an older embedded one.
  • The iOS, Android, and Flutter SDKs expose the equivalent option. The file format is identical across platforms.

Using the SDK

Pinning is already active, ordinary requests are validated with no extra code:

// This request is pinned inside the TLS handshake. A pin mismatch fails it. const response = await fetch('https://api.example.com/data');

The JavaScript API is for observing that enforcement. A common pattern is to hold first requests until the signed configuration is verified, and to log validation events:

import TrustPin from '@trustpin/react-native'; // Fail-closed readiness gate: resolves once the configuration is verified. try { await TrustPin.awaitConfiguration(10_000); } catch (error) { // Do NOT fall through to an unpinned client, treat this as a hard stop. console.error('TrustPin configuration unavailable', error); } // Definitive pin verdicts (domain + code + timestamp; no certificate material). const subscription = TrustPin.onValidationEvent(event => { if (event.code) { console.warn(`Pinning rejected ${event.domain}: ${event.code}`); } }); // subscription.remove() when you are done.

Fail-closed readiness: awaitConfiguration(timeoutMs?) resolves once the signed configuration is fetched, verified, and active, and rejects with FETCH_CERTIFICATE_TIMEOUT on timeout (the native side clamps the timeout to 10–120 s). isConfigurationLoaded() is a non-throwing Promise<boolean> status check. Skip the gate and the first pinned request simply waits for the configuration to become ready.


Validating Connections

validateConnection() is the recommended way to check a host against the active configuration without going through your HTTP client. It performs a TLS handshake and verifies the leaf certificate against your pins.

import TrustPin from '@trustpin/react-native'; async function checkServer(host: string) { try { await TrustPin.validateConnection(host, 443, 5_000); console.log('Connection is allowed by the configured pins.'); } catch (error) { console.error(`Validation failed: ${error.code} - ${error.message}`); } }
ParameterTypeDefault
hoststringNone (required)
portnumber443
timeoutMsnumbernative default

API Reference

Import the default export, or named members:

import TrustPin, { TrustPinError, TrustPinErrorCodes } from '@trustpin/react-native';
MethodDescription
awaitConfiguration(timeoutMs?)Fail-closed readiness gate. Resolves once the signed configuration is fetched, verified, and active. Rejects FETCH_CERTIFICATE_TIMEOUT on timeout. The native side clamps the timeout to 10–120 s.
isConfigurationLoaded()Promise<boolean> indicating whether a validated configuration is currently loaded.
validateConnection(host, port?, timeoutMs?)Manually validates the TLS certificate of host:port against the pins (port defaults to 443). Resolves on success, rejects with a stable code otherwise.
setLogLevel(level)Sets native log verbosity: 'none' | 'error' | 'info' | 'debug'.
onValidationEvent(listener)Subscribes to definitive pin verdicts. Returns { remove() }.
onLogEvent(listener)Subscribes to native TrustPin log output. Returns { remove() }.

Events buffered before JavaScript is alive (cold-start pin failures) are replayed to the first subscriber of each stream.

interface TrustPinValidationEvent { domain: string; code: string | null; // null = success; else a failure code timestampMs: number; } interface TrustPinLogEvent { level: 'error' | 'info' | 'debug'; message: string; timestampMs: number; }

Monitoring Pin Validation

TrustPin.onValidationEvent() surfaces the native SDKs’ validation telemetry, the signal to use for reporting suspected MITM attempts to your backend:

const subscription = TrustPin.onValidationEvent(event => { if (event.code) { // Definitive failure verdicts only: PINS_MISMATCH, ALL_PINS_EXPIRED, // DOMAIN_NOT_REGISTERED (strict mode). Events carry the domain, // failure code, and timestamp. No certificate material. securityMonitor.report(event); } }); // subscription.remove() when you are done.

Events are observe-only: the verdict is decided before the event is emitted, so a listener cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no events. Verdicts buffered before JavaScript is alive (cold-start pin failures) are replayed to the first subscriber.


Logging

Set the desired verbosity to capture SDK logs. In bare projects the native init helper also accepts a startup log level (start(logLevel:) / start(this, ...)); in Expo, set the logLevel plugin prop.

import TrustPin from '@trustpin/react-native'; await TrustPin.setLogLevel('info');

Available levels: none, error, info, debug.

Log Stream

TrustPin.onLogEvent() routes native SDK log output into your app’s logging pipeline:

const subscription = TrustPin.onLogEvent(event => { myLogger.log(`[${event.level}] ${event.message}`); }); // subscription.remove() when you are done.

Error Handling

Every rejection carries a stable string code. JS-side failures are a TrustPinError; native rejections carry the same { code, message } shape, so error.code works uniformly:

import TrustPin, { TrustPinErrorCodes } from '@trustpin/react-native'; try { await TrustPin.validateConnection('api.example.com'); } catch (error) { switch (error.code) { case TrustPinErrorCodes.PINS_MISMATCH: // certificate matched no pin, possible MITM case TrustPinErrorCodes.DOMAIN_NOT_REGISTERED: // strict mode: domain not in your config case TrustPinErrorCodes.ALL_PINS_EXPIRED: // every pin for the domain has expired default: console.error(error.code, error.message); } }

Common codes:

CodeMeaning
PINS_MISMATCHServer certificate doesn’t match any active pin
ALL_PINS_EXPIREDEvery pin for the domain has expired. Rotate pins in the dashboard
DOMAIN_NOT_REGISTEREDStrict mode and the host isn’t in the configuration
INVALID_SERVER_CERTServer returned an unparseable certificate
FETCH_CERTIFICATE_TIMEOUTConnection timed out during certificate retrieval
INVALID_PROJECT_CONFIGBad credentials or invalid configuration (also raised if the native init helper was never wired)
INVALID_ARGUMENTSAn argument to an API call was invalid

Android additionally surfaces UNSUPPORTED_DEVICE, SETUP_IN_PROGRESS, LOCK_TIMEOUT, and SSL_CONTEXT_SETUP_FAILED.


Best Practices

Setup & Initialization

  1. Configure in native code, using the Expo config plugin or the bare init helper, so pinning is active before any JavaScript networking.
  2. Gate first requests on awaitConfiguration() to fail closed before the first pinned request.
  3. Set the log level early to capture initialization output.
  4. Don’t fall through to an unpinned client if configuration is unavailable. Treat it as a hard stop.

Security

  1. Use strict mode in production.
  2. Prefer SPKI pinning; rotate pins in the dashboard before they expire.
  3. Monitor pin validation failures via TrustPin.onValidationEvent().
  4. Keep credentials outside public source control. Prefer app.config.js with environment variables (Expo) or ship the native config files (TrustPin-Info.plist / trustpin.json). Credentials never enter the JavaScript bundle.
  5. Use HTTPS for all pinned domains.
  6. Ship an embedded configuration only if the app is protected by RASP, and regenerate it in CI on every release.

Performance

  1. Configuration is cached with automatic refresh.
  2. The JavaScript API is observe-only, there is no per-request overhead beyond the native TLS validation.
  3. Use minimal log levels in production.

Complete Documentation

For the full API reference and advanced configuration, visit:

TrustPin React Native API Reference 


Resources