A local Expo module that bridges Plaud Embedded's SDK into React Native on iOS and Android. It exposes BLE scan/connect, on-device recording events, file listing, and audio export to JavaScript. implementations.
modules/plaud-sdk/— the module itself. This is the piece you drop into another project. Seemodules/plaud-sdk/README.mdfor the terse module-level notes.react-native-demo/— a reference Expo (SDK 57) app wiring the module end to end: scan → connect → list → export → transcribe.src/app/index.tsxis the canonical usage example.
The demo app is included in the Plaud Embedded Module as reference for implementing the module in your own app and seeing how the module works.
git clone https://github.com/Plaud-AI/embedded-react-native.gitcd react-native-demo
npm i
brew install cocoapods #if not already installed
cp .env.example .envYou can retrieve your environment credentials from the developer portal and retrieve a token from our API playground
npx expo prebuild -p ios
open ios/reactnativedemo.xcworkspaceIn XCode, make sure to include your Apple developer credentials and certificate.
npx expo prebuild -p android
npx expo run:androidThen run on a physical device to test out the demo app with your Plaud devices.
The module is built on Expo's module system, so the smoothest path is an Expo (or Expo-prebuild) app. Bare React Native works too — you just need the Expo Modules infrastructure installed first.
| Tool | Notes |
|---|---|
| Node.js | v20+ (v24 used here) |
| Xcode | 16.x+, with a physical iPhone + Apple ID |
| CocoaPods | brew install cocoapods |
| An Expo-based RN app | Expo SDK 52+ recommended (this repo uses SDK 57) |
Skip this if your app already uses Expo. For a bare React Native app, install the Expo Modules
runtime once — it's what provides requireNativeModule and the autolinking PlaudSdk
depends on:
npx install-expo-modules@latestThe Skill has context on the Plaud Embedded plugin to help you implement this plugin for your react-native app.
npx skills add Plaud-AI/embedded-react-nativePlace the module where Expo autolinking looks — a modules/ folder at your project root:
cp -R modules/plaud-sdk /path/to/your-app/modules/plaud-sdkThe module's expo-module.config.json is what makes autolinking pick it up, so Metro and the
native build resolve the plaud-sdk import automatically.
The one thing to add is a TypeScript path mapping so
the import { PlaudSdk } from 'plaud-sdk' type-resolves. In your app's tsconfig.json:
No
npm installis needed for the module — it isn't an installed package. Autolinking discovers it frommodules/at prebuild/build time.
These live in the app's config (not the module) so they survive expo prebuild. Add them
under expo.ios.infoPlist:
{
"expo": {
"ios": {
"infoPlist": {
"NSBluetoothAlwaysUsageDescription": "Plaud uses Bluetooth to connect to your recorder and sync recordings.",
"UIBackgroundModes": ["bluetooth-central"]
}
}
}
}# iOS
npx expo prebuild -p ios # regenerates ios/ from app.json and runs pod install
npx expo run:ios --device # build + install on a connected iPhone
# Android
npx expo prebuild -p android # regenerates android/ from app.json
npx expo run:android # build + install on a connected handsetRe-run expo prebuild after any native config change. If you're in a bare app that manages
ios/ or android/ by hand, run pod install from ios/ instead — autolinking still
discovers the module on both platforms, with no settings.gradle or Podfile edits needed.
import { PlaudSdk, isAvailable } from 'plaud-sdk';
if (!isAvailable) {
// web / iOS simulator — the native module isn't linked. Degrade gracefully.
}
// 1. Initialise with a per-user JWT (see "Tokens" below).
await PlaudSdk.initSDK({
userAccessToken, // per-user Bearer JWT
customDomain: 'platform-us.plaud.ai', // domain only, no https://
userId: 'your-app-user-id', // reused as the connect deviceToken
});
// 2. Subscribe to the event stream — this is where results land.
const subs = [
PlaudSdk.addListener('scanResult', ({ devices }) => {/* show devices */}),
PlaudSdk.addListener('connectState', ({ connected, failed }) => {
if (connected) PlaudSdk.getFileList(); // ask for recordings once connected
}),
PlaudSdk.addListener('fileList', ({ files }) => {/* show recordings */}),
PlaudSdk.addListener('exportProgress', ({ progress, message }) => {/* progress UI */}),
];
// 3. Drive it.
await PlaudSdk.startScan();
// user taps a device from scanResult:
await PlaudSdk.connectBleDevice({ uuid: device.uuid });
// user taps a file from fileList:
const { outputPath } = await PlaudSdk.exportAudio({ sessionId, format: 'mp3' });
// 4. Clean up listeners on unmount.
subs.forEach((s) => s.remove());The demo's react-native-demo/src/app/index.tsx is a complete, production-shaped version of
this (React state, error handling, live-recording banners). Read it before building your own
screen.
initSDK needs a per-user access token (a Bearer JWT). The SDK does not mint it — that's
an app/backend concern. Mint it via Plaud's partner OAuth flow on your backend and hand it to
the client. For local testing you can paste one via EXPO_PUBLIC_PLAUD_ACCESS_TOKEN (Expo
inlines EXPO_PUBLIC_* at build time).
Once a recording is exported to a local file, uploading and transcribing it is plain HTTP —
not part of this native module. The demo shows the full flow in
react-native-demo/src/lib/plaud-transcription.ts (presigned S3 upload → submit → poll).
The demo calls the Plaud platform API directly from the device with
EXPO_PUBLIC_*credentials, which are extractable from the bundle. That's fine for a demo, but in production the transcription API key and upload must live behind a backend.
The module is three layers stacked on top of each other. A JS call travels down; native events travel back up.
your React Native code
│ import { PlaudSdk, isAvailable } from 'plaud-sdk'
▼
┌─────────────────────────────┐
│ JS layer (src/*.ts) │ requireNativeModule('PlaudSdk'), fully typed,
│ │ degrades to a no-op Proxy where unlinked
└─────────────────────────────┘
│ Expo Modules bridge (AsyncFunction / Events)
┌──────────────────┬──────────────────────┐
│ iOS │ Android │
│ PlaudSdkModule │ PlaudSdkModule │ same Name("PlaudSdk"),
│ .swift │ .kt │ same events & payloads
├──────────────────┼──────────────────────┤
│ 3 .xcframeworks │ plaud-sdk.aar │ Plaud native SDK
│ BLE/Device/WiFi │ (+ .so per ABI) │
└──────────────────┴──────────────────────┘
1. JS layer (src/index.ts, src/PlaudSdk.types.ts).
requireNativeModule('PlaudSdk') resolves the native module at runtime. It's called lazily
inside a try/catch on iOS and Android, so the module never throws at import time. Two
exports matter:
isAvailable—trueonly when the native module is linked and callable (a physical device). Guard every call site with it.PlaudSdk— the typed handle. When the native module is absent (web, iOS simulator), it's aProxywhose methods reject and whoseaddListeneris a harmless no-op, so shared code doesn't need platform branches everywhere.
2a. Plaud native SDK on iOS (ios/Frameworks/*.xcframework).
Three precompiled binary frameworks — PlaudBleSDK, PlaudDeviceBasicSDK, PlaudWiFiSDK —
vendored by ios/PlaudSdk.podspec (vendored_frameworks). CocoaPods embeds and code-signs
them automatically.
2b. Plaud native SDK on Android (android/libs/plaud-sdk.aar).
One precompiled AAR, consumed by android/build.gradle; its per-ABI .so files ship inside
it and are packaged automatically. Because the AAR carries no POM, its transitive
dependencies (Retrofit, OkHttp, Gson, BouncyCastle, Java-WebSocket, Conscrypt, Timber,
slf4j/logback, Guava, coroutines) are declared by hand in that build.gradle. Bluetooth permissions come from the
AAR's own manifest via manifest merging, so app.json needs nothing.