@superutils/expo-binary-module
An Expo native module for running and managing long-running native binaries on Android.
The module is designed for applications that need to bundle a native executable inside the Android application and control its lifecycle from React Native. It runs the executable as an Android foreground service, supports configurable environment variables, persistent auto-start configuration, startup detection, and customizable foreground-service notifications.
Android only.
Table of Contents
- ChangeLog
- Features
- Requirements
- Installation
- Expo Configuration
- Basic Usage
- Start Options
- Status
- Updating the Notification
- Stopping the Binary
- Crash Detection
- Binary Output
- Auto-start and Android Boot
- Complete Example
- Development
- Platform Support
- License
Features
- Run bundled native binaries from React Native
- Keep binaries running independently of the React Native JS runtime
- Android foreground service support
- Start and stop the binary programmatically
- Configure environment variables
- Generate random environment values at runtime
- Hide binary-only environment values from the React Native side
- Persist auto-start configuration
- Automatically restart configured binaries after:
- Android device reboot
- Locked boot
- Application update
- Detect successful startup using configurable startup log text
- Detect process crashes and exits
- Expose the current binary status to React Native
- Customize the foreground-service notification
- Show binary output in Android logs
Requirements
- Expo SDK 56+
- Android 8.0+ (API 26+)
- A native Expo development build
This module cannot be used with the standard Expo Go application because it contains custom native Android code.
Installation
Install the package in your Expo project:
yarn add @superutils/expo-binary-moduleor:
npm install @superutils/expo-binary-moduleThen generate the native Android project:
npx expo prebuildBuild the Android application:
npx expo run:androidExpo Configuration
The package includes an Expo config plugin that configures the native Android project during expo prebuild.
The plugin is responsible for configuring the native Android project and adding the configured jniLibs directory to the application's native library sources.
jniLibs Configuration
The module requires the directory containing the native binaries to be configured through the Expo plugin.
Add the module to the plugins section of your app.json:
{
"expo": {
"plugins": [
[
"@superutils/expo-binary-module",
{
"jniLibs": "./path/to/jniLibs"
}
]
]
}
}jniLibs accepts comma-separated paths (eg: "./path/to/jniLibs,./path/to/other/jniLibs"). If more than one path is provided, all files from all specified paths will be merged and copied into the APK.
The directory should contain native binaries organized by Android ABI:
jniLibs/
├── arm64-v8a/
│ └── libmybinary.so
├── armeabi-v7a/
│ └── libmybinary.so
└── x86_64/
└── libmybinary.soThe binary name passed to start() must match the binary available in the application's native library directory.
For example:
await BinaryModule.start({
binaryName: 'libmybinary.so',
})After changing the Expo configuration, regenerate the native project:
npx expo prebuildIf you need to completely regenerate the native Android project:
npx expo prebuild --cleanBasic Usage
Import the module:
import BinaryModule from '@superutils/expo-binary-module'Starting a Binary
The simplest way to start a binary is:
const started = await BinaryModule.start({
binaryName: 'libmybinary.so',
})
if (!started) {
console.error('Failed to start binary')
}When no startupText is configured, start() returns after the native process has been successfully launched.
Stopping a Binary
await BinaryModule.stop()Checking Status
const status = BinaryModule.getStatus()
console.log(status)The returned status describes the current state of the native binary process.
Accessing Exposed Environment Variables
Environment variables that are not marked as binaryOnly can be exposed to React Native.
const env = BinaryModule.getExposedEnv()
console.log(env)Values marked as binaryOnly are passed to the native binary but are not exposed through this API.
Start Options
start() accepts a StartOptions object.
type StartOptions = {
autoStart?: boolean
autoStop?: AutoStopOptions | null
awaitStartDelay?: number
binaryName: string
env?: BinaryEnv
ipcOptions?: IpcOptions
notification?: Partial<NotificationOptions>
startupText?: string
startupTimeout?: number
}binaryName
The name of the native binary to execute.
{
binaryName: 'libmybinary.so'
}The binary must be available in the application's native library directory.
autoStart
Determines whether the binary should automatically start again after Android restarts or the application is updated.
{
autoStart: true,
binaryName: 'libmybinary.so',
}When enabled, the module persists the startup configuration.
The Android boot receiver uses that configuration to restart the binary after:
BOOT_COMPLETEDLOCKED_BOOT_COMPLETEDMY_PACKAGE_REPLACED
When autoStart is disabled, the persisted auto-start configuration is removed.
autoStop
Configure to auto-stop and restart the binary based on device network battery status.
type AutoStopOptions = {
/** Auto stop binary if device is on airplane mode */
airplaneMode?: boolean
/** Auto stop binary if battery level is below provided level/percentage. Default: 10 (=**10%**) */
batteryLevelBelow?: number
/** Auto stop binary if battery is not in charging state */
batteryNotCharging?: boolean
/** Auto stop binary if network is metered. Default: `false` */
metered?: boolean
/** Auto stop if matches any of the network types provided. Default: "['cellular']" */
networkTypes?: DeviceNetworkType[]
/** Auto stop binary if device is on power save mode */
powerSaveMode?: boolean
}Environment Variables
Environment variables are configured using EnvEntry objects.
type EnvEntry = {
value: string
binaryOnly?: boolean
random?: boolean
prefix?: string
}Example:
{
binaryName: 'libmybinary.so',
env: {
PORT: {
value: '1234',
},
API_KEY: {
value: 'secret-value',
binaryOnly: true,
},
},
}binaryOnly
When binaryOnly is true, the environment variable is passed to the native binary but is not exposed back through the module's environment API.
This is useful for values that the binary needs but React Native should not receive.
{
API_KEY: {
value: 'secret-value',
binaryOnly: true,
},
}When binaryOnly is false or omitted, the resolved value is available to the React Native side.
random
When random is enabled, the module generates a random value at runtime instead of using value.
{
SECRET: {
value: '',
random: true,
},
}The generated value is Base64 URL-safe encoded without padding.
prefix
A prefix can be added to generated random values.
{
SECRET: {
value: '',
random: true,
prefix: 'myapp-',
},
}The resulting environment variable will have the form:
myapp-<random-value>Startup Detection
By default, the module considers the process started once the native process has been successfully launched.
For binaries that expose a startup message in their output, startupText can be used to wait for a specific log message before reporting STARTED.
const started = await BinaryModule.start({
binaryName: 'libmybinary.so',
startupText: 'Binary listening on',
})The module reads the binary's combined stdout and stderr output and looks for the configured text.
Startup detection waits for up to 10 seconds.
If the text is observed:
STARTING
|
v
STARTEDIf the text is not observed within the timeout:
- the process is terminated
- the status becomes
CRASHED - the notification reports
Startup timeout start()returnsfalse
Without startupText, start() returns after the process has been launched successfully.
Notifications
The binary runs as an Android foreground service, so Android requires a persistent foreground-service notification while it is running.
The default notification uses the application's icon and displays:
<App Name>: Foreground Service
RunningThe notification can be customized using NotificationOptions.
type NotificationOptions = {
title?: string
text?: string
subText?: string
ongoing?: boolean
silent?: boolean
progress?: {
max: number
current: number
indeterminate?: boolean
}
}title
Overrides the default notification title.
{
title: 'My Binary',
}If no title is provided, the default is:
<App Name>: Foreground Servicetext
Sets the notification's primary text.
Newlines can be used when multiline notification content is desired.
{
text: 'Binary is running\nListening for connections',
}subText
Sets Android's notification subtext.
{
subText: 'This is a subtext',
}ongoing
Controls whether Android treats the notification as an ongoing notification.
{
ongoing: true,
}When enabled, the notification cannot normally be dismissed by the user while the service is running.
silent
Controls whether the notification is silent.
{
silent: true,
}progress
Displays progress in the notification.
{
progress: {
max: 100,
current: 65,
indeterminate: false,
},
}For an indeterminate operation:
{
progress: {
max: 100,
current: 0,
indeterminate: true,
},
}Status
The module exposes the following statuses:
enum Status {
CRASHED,
NEVER_STARTED,
STARTED,
STARTING,
STOPPING,
STOPPED,
}The JavaScript representation uses the corresponding lowercase string values exposed by the native module.
A typical lifecycle is:
NEVER_STARTED
|
v
STARTING
|
v
STARTED
|
v
STOPPING
|
v
STOPPEDIf the process exits unexpectedly:
STARTED
|
v
CRASHEDUpdating the Notification
The foreground-service notification can be updated without restarting the binary.
BinaryModule.setNotification({
title: 'My Binary',
text: 'Listening for connections',
})For example:
BinaryModule.setNotification({
text: 'Syncing\nPreparing files...',
progress: {
max: 100,
current: 42,
},
})The notification options passed to setNotification() replace the corresponding notification values.
Stopping the Binary
await BinaryModule.stop()Stopping the binary:
- terminates the native process
- clears its exposed environment values
- changes the status to
STOPPED - removes the foreground notification
- stops the Android foreground service
If auto-start configuration is associated with the running process, stopping the service also clears the persisted configuration so that the binary is not unexpectedly restarted by a later boot event.
Crash Detection
The module monitors the native process after it starts.
If the process exits unexpectedly, the module:
- clears the process reference
- clears exposed environment values
- changes the status to
CRASHED - updates the foreground notification
The process watcher also protects against an older process changing the state of a newer process if a binary is restarted before the previous process watcher finishes.
Binary Output
The binary's stdout and stderr are combined and read by the service.
Output is written to Android's log for each line:
MM-DD HH:MM:SS.XXX XXXX XXXX D [ExpoBinaryService]: [BinaryLog][libbinary.so] <LOG_FROM_THE_BINARY>This output (<LOG_FROM_THE_BINARY>) is also used by startupText when startup detection is enabled.
For adb connected devices you can see the log by running:
adb logcat | grep ExpoBinaryModuleAuto-start and Android Boot
When autoStart is enabled, the module persists the startup configuration as JSON.
The included Android boot receiver listens for:
BOOT_COMPLETED
LOCKED_BOOT_COMPLETED
MY_PACKAGE_REPLACEDWhen one of these events occurs, the receiver loads the persisted configuration and starts the foreground service again.
This allows a configured binary to start automatically after:
- a normal device reboot
- a device boot before user unlock
- an application update
No server or external service is required for this behavior.
Complete Example
A complete example for a long-running binary:
import BinaryModule from '@superutils/expo-binary-module'
const started = await BinaryModule.start({
binaryName: 'libmybinary.so',
autoStart: true,
startupText: 'Binary listening',
env: {
PORT: {
value: '1234',
},
INSTANCE_ID: {
value: '',
random: true,
prefix: 'myapp-',
binaryOnly: true,
},
},
notification: {
title: 'My Binary',
text: 'Starting...',
ongoing: true,
silent: true,
},
})
if (!started) {
console.error('Binary failed to start')
}Once the binary reports its startup message, the notification can be updated:
BinaryModule.setNotification({
text: 'Listening for connections',
})When the binary is no longer needed:
await BinaryModule.stop()Development
Build the module:
yarn buildInspect the package contents before publishing:
npm pack --dry-runThe package needs to contain the generated JavaScript in build/, the Android native implementation, the Expo config plugin, and the package metadata required by Expo modules.
Platform Support
| Platform | Support |
|---|---|
| Android | Yes |
| iOS | No |
| Web | No |
This package intentionally provides Android-specific functionality for running long-lived native processes.
License
MIT
Enumerations
Type Aliases
- AppStateStatus
- AutoStopOptions
- BinaryEnv
- BinaryModuleEvents
- EnvEntry
- IpcOptions
- NotificationAction
- NotificationID
- NotificationManager
- NotificationOptions
- StartOptions