create-expo-module
A cookbook for scaffolding a local native module with a typed TypeScript surface - the supported path when Expo SDK packages and third-party libraries do not cover your integration.
Search across all documentation pages
A cookbook for scaffolding a local native module with a typed TypeScript surface - the supported path when Expo SDK packages and third-party libraries do not cover your integration.
Quick-reference recipe card - copy-paste ready.
# From your Expo app root
npx create-expo-module@latest modules/attestation
# Answer prompts: module name, description, platforms (ios + android)
cd modules/attestation
npm install
npm run build # compiles TypeScript surface
# Wire into the app (workspace or file dependency)
cd ../..
npm install ./modules/attestation
# Regenerate native projects
npx expo prebuild --clean
npx expo run:ios// App usage after scaffold
import AttestationModule from "attestation";
const result = await AttestationModule.isDeviceTrustedAsync();When to reach for this:
packages/attestation.npx create-expo-app@latest AttestationApp --template blank-typescript@sdk-57 --yes
cd AttestationApp
npx create-expo-module@latest modules/device-trustTypical output layout:
AttestationApp/
├── app.json
├── package.json
├── App.tsx
└── modules/
└── device-trust/
├── android/
│ └── src/main/java/expo/modules/devicetrust/DeviceTrustModule.kt
├── ios/
│ └── DeviceTrustModule.swift
├── src/
│ ├── DeviceTrustModule.ts
│ └── index.ts
├── expo-module.config.json
└── package.json// modules/device-trust/ios/DeviceTrustModule.swift
import ExpoModulesCore
public class DeviceTrustModule: Module {
public func definition() -> ModuleDefinition {
Name("DeviceTrust")
AsyncFunction("isDeviceTrustedAsync") { () -> Bool in
// Replace with DeviceCheck / App Attest integration
#if targetEnvironment(simulator)
return false
#else
return true
#endif
}
}
}// modules/device-trust/android/src/main/java/expo/modules/devicetrust/DeviceTrustModule.kt
package expo.modules.devicetrust
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class DeviceTrustModule : Module() {
override fun definition() = ModuleDefinition {
Name("DeviceTrust")
AsyncFunction("isDeviceTrustedAsync") {
// Replace with Play Integrity API
!Build.FINGERPRINT.contains("generic")
}
}
}// modules/device-trust/src/DeviceTrustModule.ts
import { requireNativeModule } from "expo-modules-core";
type DeviceTrustModuleType = {
isDeviceTrustedAsync: () => Promise<boolean>;
};
export default requireNativeModule<DeviceTrustModuleType>("DeviceTrust");// modules/device-trust/src/index.ts
export { default } from "./DeviceTrustModule";// AttestationApp/package.json (excerpt)
{
"dependencies": {
"device-trust": "file:./modules/device-trust",
"expo": "~57.0.4",
"expo-dev-client": "~6.0.0",
"react": "19.2.3",
"react-native": "0.86.0"
}
}// App.tsx
import { useEffect, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import DeviceTrust from "device-trust";
export default function App() {
const [trusted, setTrusted] = useState<boolean | null>(null);
useEffect(() => {
DeviceTrust.isDeviceTrustedAsync().then(setTrusted);
}, []);
return (
<View style={styles.container}>
<Text style={styles.title}>Device Trust Module</Text>
<Text>Trusted: {trusted === null ? "…" : trusted ? "yes" : "no"}</Text>
<Button title="Re-check" onPress={() => DeviceTrust.isDeviceTrustedAsync().then(setTrusted)} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24, gap: 8 },
title: { fontSize: 20, fontWeight: "700" },
});# Build module TS, prebuild, run
cd modules/device-trust && npm run build && cd ../..
npx expo install expo-dev-client
npx expo prebuild
npx expo run:iosWhat this demonstrates:
create-expo-module generates matching Swift/Kotlin/TypeScript skeletons with correct Name("DeviceTrust") registration.requireNativeModule loads the JSI binding after autolinking compiles native code.file:./modules/... dependency keeps the module local - publish later if reused across repos.If the module needs entitlements, add app.plugin.js inside the module package and register it in the app's app.config.ts. See Config Plugins.
{
"platforms": ["ios", "android"],
"ios": {
"modules": ["DeviceTrustModule"]
},
"android": {
"modules": ["expo.modules.devicetrust.DeviceTrustModule"]
}
}Autolinking reads this file during npx expo prebuild and wires Gradle/Pod entries - no manual linking in CNG apps.
| Layout | Path | Dependency |
|---|---|---|
| Single app | modules/device-trust | "file:./modules/device-trust" |
| Monorepo package | packages/device-trust | "workspace:*" in app package.json |
| Published npm | external registry | "device-trust": "^1.0.0" |
For monorepo autolinking nuances, see Autolinking & expo-modules-core.
Expo Module (create-expo-module) | RN TurboModule (Codegen) |
|---|---|
| Expo autolinking + config plugins | RN-first; manual Codegen setup |
Swift/Kotlin ModuleDefinition DSL | C++/Java/ObjC with spec files |
| Best for SDK 57 Expo apps | Best for RN-core libraries without expo |
Most app teams should start with Expo Modules API. Reach for Writing a TurboModule when sharing C++ across platforms or upstreaming to RN community.
Per Native Module Rules, document:
npm run build in the module - app imports stale build/ output. Fix: Add "prepare": "expo-module build" or build in CI before app compile.Name("DeviceTrust") and requireNativeModule("DeviceTrust") - runtime throw. Fix: Keep names identical across Swift, Kotlin, and TS.npx expo prebuild --clean.| Alternative | Use When | Don't Use When |
|---|---|---|
| Expo SDK package | Feature exists upstream | Proprietary API |
| Third-party npm native lib | Maintained community bridge | Unmaintained or no New Arch |
| Expo Module (this page) | Thin custom bridge in Expo app | Heavy C++ shared core |
| TurboModule + Codegen | RN ecosystem library | Standard Expo CNG app integration |
Yes, in bare React Native apps with expo-modules-core installed via npx install-expo-modules. Expo prebuild is still the easiest wiring path.
Yes. Use workspace:* and enable autolinkingModuleResolution - see autolinking page.
Use Events("onTrustChanged") in ModuleDefinition and EventEmitter on the JS side - same pattern as expo-sensors.
Only when the module requires permissions, entitlements, Gradle dependencies, or manifest metadata not covered by autolinking defaults.
Complete the scaffold, add README and tests, run npm publish from modules/device-trust. Consumers npx expo install device-trust and register any plugin.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026