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.

📚 Part of our React Native Series This post continues our ongoing exploration of React Native topics. If you want to check out the beginning of this series, here’s our first article: Bridging React Native and Rust via JSI .

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:

  1. Update their Info.plist with usage descriptions and background modes. (iOS)
  2. Add required permissions in the AndroidManifest.xml. (Android)
  3. 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:

  1. Firstly, the explicitly defined parameters in app.json or app.config.js.
  2. If no parameters are set, it falls back to the iOS native project’s Info.plist.
  3. 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;
  });

⚒️ Manual Step (CLI Users): Edit Info.plist to add the usage descriptions (NSBluetoothAlwaysUsageDescription, NSLocalNetworkUsageDescription), background modes, and bonjour services.

❇️ Plugin Advantage (Expo): Automatically configures these fields for you and let’s you customize them.

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;
};

⚒️ Manual Step (CLI Users): Add Bluetooth, networking, and location permissions to your AndroidManifest.xml, plus handle packaging configurations for .so libraries.

❇️ Plugin Advantage (Expo): The plugin does it all by default—just install and go.

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

Read more
Updates
Product
April 22, 2025
Flutter for Web Reaches General Availability in Record Time
by
Rae McKelvey
As of Ditto Flutter SDK v4.10.1, Flutter for Web has reached General Availability. In October 2024, when we shipped 4.8, Ditto developers started asking if we could extend Ditto to support Flutter web apps – and we took that challenge to heart. With the GA, let's take a closer look at what's included today, some key considerations, and how you can get started today.
Product
April 14, 2025
New Ditto Quickstart Apps - Get Started in Under 10 Minutes
by
Skyler Jokiel
We’re excited to introduce Ditto Quickstart Applications — a fast, easy way to experience Ditto’s powerful offline-first syncing in action. These Quickstart apps are basic task list demos that let you get up and running with Ditto in under 10 minutes.