Published OnApril 24, 2025April 24, 2025
Engineering with Expo: Expanding Our JavaScript SDK
In this article, we’ll detail how we introduced Expo Development Builds support for Ditto React Native and how the new Expo plugin helps developers skip several manual setup steps that React Native CLI users typically face.

In this article, we’ll detail how we introduced Expo Development Builds support for Ditto React Native and how the new Expo plugin helps developers skip several manual setup steps that React Native CLI users typically face. We do rely on native modules—so Expo Go (designed mostly for rapid prototyping) remains outside of our scope—but if you’re working on a custom development build of an Expo app, you can now easily integrate with Ditto’s resilient mobile database, enabling real-time, peer-to-peer synchronization without having to do the mobile configuration bits by hand.
Why We Embraced Expo Plugins
Expo is the default way to build with React Native. Recognizing this, we aimed to save developers time by creating an Expo plugin. Whether building an Expo app or module, this plugin simplifies the process. Typically, React Native CLI users must manually:
- Update their
Info.plist
with usage descriptions and background modes. (iOS) - Add required permissions in the
AndroidManifest.xml
. (Android) - Set up any additional build or packaging configurations.
Starting with Ditto 4.10.0, all that is automated for Expo developers.
Setting Up the Plugin
Below, we’ve split the plugin into key sections so you can see exactly what it does. Each code snippet targets a specific platform and will automatically apply a set of configurations when clients apply the plugin and run npx expo prebuild
, in a process called Continuous Native Generation (CNG).
iOS Configuration
The following logic injects the custom Info.plist
configuration and Background Modes required by certain Ditto iOS modules.
Optional properties such as bluetoothUsageDescription
and localNetworkUsageDescription
are designed to be configurable. Expo plugins facilitate this by allowing properties to be passed directly. However, this flexibility introduces a question of precedence: when multiple descriptions are modified simultaneously, which should take priority? To maintain consistency with other Expo packages, we’ve established the same hierarchy as with Expo Camera:
- Firstly, the explicitly defined parameters in
app.json
orapp.config.js
. - If no parameters are set, it falls back to the iOS native project’s
Info.plist
. - If no values are found, Ditto uses its default values.
Additionally, it’s prudent to check for existing background modes to prevent the introduction of duplicates, both now and in the future.
import { ConfigPlugin, withInfoPlist } from '@expo/config-plugins';
const BLE_USAGE = 'Uses Bluetooth to connect and sync with nearby devices.';
const LAN_USAGE = 'Uses WiFi to connect and sync with nearby devices.';
type DittoConfig = {
bluetoothUsageDescription?: string;
localNetworkUsageDescription?: string;
};
const withDittoIOS: ConfigPlugin<DittoConfig> = (expoConfig, props) =>
withInfoPlist(expoConfig, (config) => {
const infoPlist = config.modResults;
infoPlist.NSBluetoothAlwaysUsageDescription =
props.bluetoothUsageDescription ??
infoPlist.NSBluetoothAlwaysUsageDescription ??
BLE_USAGE;
infoPlist.NSLocalNetworkUsageDescription =
props.localNetworkUsageDescription ??
infoPlist.NSLocalNetworkUsageDescription ??
LAN_USAGE;
if (!Array.isArray(infoPlist.NSBonjourServices)) {
infoPlist.NSBonjourServices = [];
}
if (!infoPlist.NSBonjourServices.includes('_http-alt._tcp.')) {
infoPlist.NSBonjourServices.push('_http-alt._tcp.');
}
if (!Array.isArray(infoPlist.UIBackgroundModes)) {
infoPlist.UIBackgroundModes = [];
}
if (!infoPlist.UIBackgroundModes.includes('bluetooth-central')) {
infoPlist.UIBackgroundModes.push('bluetooth-central');
}
if (!infoPlist.UIBackgroundModes.includes('bluetooth-peripheral')) {
infoPlist.UIBackgroundModes.push('bluetooth-peripheral');
}
return config;
});
Android Configuration
The Android plugin logic is responsible for injecting the permissions and packaging rules that Ditto requires to operate via its native Android modules.
We decided to ship expo-build-properties
with the plugin to make use of the withBuildProperties
mod but also to support backward compatibility with older Expo versions when applying the plugin, as reflected in our documentation.
In addition to library packaging, we automatically append the necessary Bluetooth, networking, and location permissions to your app’s AndroidManifest.xml
.
To avoid duplication or permission conflicts, we first inspect the existing manifest and only inject permissions that aren’t already present. Just as with the iOS configuration, this ensures a clean and idempotent config, whether you’re applying the plugin once or re-running npx expo prebuild
later in development.
import { ConfigPlugin, withAndroidManifest } from '@expo/config-plugins';
import { withBuildProperties } from 'expo-build-properties';
const withDittoAndroid: ConfigPlugin = (expoConfig) => {
const architectures = ['x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'];
const libraries = [
'libjsi.so',
'libdittoffi.so',
'libreact_nativemodule_core.so',
'libturbomodulejsijni.so',
'libreactnative.so',
];
expoConfig = withBuildProperties(expoConfig, {
android: {
packagingOptions: {
pickFirst: architectures.flatMap((arch) =>
libraries.map((lib) => `lib/${arch}/${lib}`)
),
},
},
});
expoConfig = withAndroidManifest(expoConfig, async (config) => {
const androidManifest = config.modResults;
const permissions = androidManifest.manifest['uses-permission'] || [];
const permissionsToAdd = [
{ name: 'android.permission.BLUETOOTH' },
{ name: 'android.permission.BLUETOOTH_ADMIN' },
{ name: 'android.permission.BLUETOOTH_ADVERTISE' },
{ name: 'android.permission.BLUETOOTH_CONNECT' },
{
name: 'android.permission.BLUETOOTH_SCAN',
attributes: { 'android:usesPermissionFlags': 'neverForLocation' },
},
{ name: 'android.permission.ACCESS_FINE_LOCATION' },
{ name: 'android.permission.ACCESS_COARSE_LOCATION' },
{ name: 'android.permission.INTERNET' },
{ name: 'android.permission.ACCESS_WIFI_STATE' },
{ name: 'android.permission.ACCESS_NETWORK_STATE' },
{ name: 'android.permission.CHANGE_NETWORK_STATE' },
{ name: 'android.permission.CHANGE_WIFI_MULTICAST_STATE' },
{ name: 'android.permission.CHANGE_WIFI_STATE' },
{
name: 'android.permission.NEARBY_WIFI_DEVICES',
attributes: { 'android:usesPermissionFlags': 'neverForLocation' },
},
];
permissionsToAdd.forEach((permission) => {
function isPermissionAlreadyRequested(
permissionName: string,
manifestPermissions: any[]
) {
return manifestPermissions.some(
(e) => e.$['android:name'] === permissionName
);
}
if (!isPermissionAlreadyRequested(permission.name, permissions)) {
const permissionObject = {
$: {
'android:name': permission.name,
...permission.attributes,
},
};
permissions.push(permissionObject);
}
});
androidManifest.manifest['uses-permission'] = permissions;
return config;
});
return expoConfig;
};
Assembling Everything
These two configurations can be assembled over the entry point of this plugin, withDitto
, where we can also see how the client plugin properties are passed down to each of them:
import { ConfigPlugin, createRunOncePlugin } from '@expo/config-plugins';
const withDitto: ConfigPlugin = (config, props = {}) => {
config = withDittoIOS(config, props);
config = withDittoAndroid(config, props);
return config;
};
export default createRunOncePlugin(withDitto, 'ditto_expo');
A Trick for Custom Plugin Folder Name
By default, Expo expects your plugin code to live in a folder named plugin
—but that felt too generic for our taste. To make things more readable and maintainable, especially in projects with multiple plugins, we renamed ours to expo-plugin
.
However, changing the folder name means Expo’s autolinking system won’t find the plugin automatically. To fix this, we ship a custom project-level app.plugin.js
file that explicitly points to the new plugin path:
module.exports = require('./react-native/expo-plugin/build');
This small tweak lets us keep a clearer folder structure without breaking Expo’s plugin resolution.
For Detailed Setup Instructions
While we’ve outlined the capabilities and advantages of our Expo plugin in this post, detailed step-by-step setup instructions are available in our public documentation. This resource provides comprehensive guidance on integrating the Ditto SDK into your Expo Dev Builds project, including:
• Enabling the Expo Config Plugin in your app.json
or app.config.js
.
• Additional configurations required for older Expo projects.
• Customizing iOS permission prompts.
For complete details, please refer to our React Native SDK documentation.
A Note on Autolinking (and a Shoutout to the Expo Team)
While building out our Expo plugin, we wanted to use a custom folder layout for our native module—one that didn’t follow the default android/
and root-level .podspec
convention. This used to be a blocker because expo-module-autolinking
only supported default paths for Android.
After opening a feature request, the Expo team responded quickly and landed a fix that allows android.sourceDir
to be set for native libraries. This gave us the flexibility to keep our internal structure clean without affecting Expo compatibility.
We were really impressed with the turnaround and attention to detail from the Expo maintainers. This kind of support is one of the reasons we’re excited to continue investing in Expo as part of our React Native ecosystem.
Our RN Expo Quickstart app
Before shipping support for Expo, we also created an internal testing app. It exercises all the background permissions, Bluetooth scanning, and local networking behaviors we rely on. That made it easy to validate that everything worked in a real device environment.
However, for developers that want to quickly test Expo with Ditto, we provide the RN Expo Quickstart app—our reference project showing the minimal setup required to get up and running with our SDK in an Expo environment.
Wrapping Up
Our new Expo plugin removes the typical friction points that React Native CLI developers deal with manually. By automating all the iOS and Android configuration, anyone using a custom Expo development build can confidently integrate our SDK—and focus on building rich experiences instead of tinkering with manifests and property list files.
If you have any questions or want to try out our SDK, drop by our documentation to see how to get started immediately. If you’re curious about how Ditto for React Native works under the hood, make sure to check out Bridging React Native and Rust via JSI.
Resources
- Ditto RN Expo Quickstart App on GitHub
- Ditto React Native Install Guide
- Ditto JS on NPM
- Expo Development Builds
- Expo Camera Plugin Example
- Bridging React Native and Rust via JSI
- Expo’s Discord channel

