Mobile — Expo Development

Expo Config Plugins for Native Module Integration

Direct answer

Config plugins are functions that modify the generated native projects during prebuild — Info.plist, AndroidManifest, gradle files, entitlements — so you can integrate native modules without maintaining ios and android folders by hand. Most maintained libraries ship their own plugin you just list in app.json; you write a custom one only when a module needs native configuration nothing covers. They're the mechanism that made 'ejecting' obsolete.

Config plugins are the least-understood part of modern Expo and the one that unlocks everything else: they're how an Expo app uses arbitrary native code while keeping its native projects generated and disposable. Here's the mental model, plus the custom plugin patterns I actually use on client work.

Key facts, with sources

  • 71% of State of React Native 2024 respondents reported using Expo's EAS Build, ahead of manual builds with Xcode (59.7%) and Android Studio (54.5%). (InfoQ)
  • Expo SDK 52, released November 12, 2024, enabled the New Architecture by default for all new projects, and Expo Go for SDK 52 and higher supports only the New Architecture. (Expo Changelog)
  • The official React Native documentation now recommends starting new apps with a framework, naming Expo, rather than initializing a bare project. (React Native documentation)
  • EAS build caching can speed up subsequent Android and iOS builds by up to 30% and is available to all users at no additional cost as of the SDK 57 cycle. (Expo Changelog)
  • Doctolib runs Expo tooling without EAS to scale developer experience on a healthcare app serving 90 million users, showing Expo's open-source tools work independently of its paid cloud services. (Doctolib Engineering (Medium))

The mental model: native projects as build output

In a modern Expo project, the ios and android directories are not source code — they're artifacts produced by npx expo prebuild from two inputs: your app config (app.json or app.config.ts) and the config plugins it references. Every plugin is a function that takes the config, applies a transformation to the native project representation, and returns it. Prebuild runs the chain and writes out complete, standard native projects.

This inversion is what keeps upgrades cheap: since nobody hand-edits the native projects, regenerating them against a new SDK version is safe. The corollary is a rule I enforce in every audit — if a team has committed manual edits inside ios or android while still running prebuild, those edits are a time bomb, because the next prebuild silently erases them. Native customization belongs in plugins, full stop.

Using library plugins: the ninety percent case

Most of the time you never write a plugin — you use one. Maintained native libraries ship plugins that handle their own integration: adding permissions strings, configuring gradle, wiring entitlements. You list them in the plugins array, pass options where supported, and rebuild. The expo-build-properties plugin deserves special mention because it covers a whole category of one-off needs — minimum SDK versions, Kotlin versions, ProGuard flags, static frameworks for iOS — that used to require custom plugins.

After editing plugins, remember that config changes require a new development build; they cannot arrive over the air. That's the workflow beat that trips up teams new to Expo: plugin change, prebuild, rebuild, then develop as usual.

app.json — library plugins plus a local custom plugin
{
  "expo": {
    "plugins": [
      "expo-camera",
      [
        "expo-build-properties",
        { "android": { "minSdkVersion": 24 } }
      ],
      "./plugins/with-bluetooth"
    ]
  }
}

Writing a custom plugin

You write a custom plugin when a native requirement isn't covered by any library plugin: a vendor SDK's manifest entries, an unusual permission string, an entitlement your auth provider needs. The config-plugins package exports typed helpers — withInfoPlist, withAndroidManifest, withEntitlementsPlist, withGradleProperties and friends — that each hand you the parsed native file as modResults to mutate.

The example below adds an iOS Bluetooth usage description and an Android permission. Note the shape: plain function, config in, config out, composing helpers. Keep each plugin small and single-purpose — I name them with the with- prefix convention and keep them in a plugins directory so their intent is auditable at a glance.

plugins/with-bluetooth.js — custom config plugin
const {
  withInfoPlist,
  withAndroidManifest,
} = require('expo/config-plugins');

module.exports = function withBluetooth(config) {
  config = withInfoPlist(config, (c) => {
    c.modResults.NSBluetoothAlwaysUsageDescription =
      'Connects to nearby devices for syncing.';
    return c;
  });

  config = withAndroidManifest(config, (c) => {
    const manifest = c.modResults.manifest;
    manifest['uses-permission'] = manifest['uses-permission'] || [];
    manifest['uses-permission'].push({
      $: { 'android:name': 'android.permission.BLUETOOTH_CONNECT' },
    });
    return c;
  });

  return config;
};

Dangerous mods and where I draw the line

The structured helpers operate on parsed representations of native files, which makes them resilient across SDK upgrades. There's also a raw escape hatch — dangerous modifications that let you rewrite arbitrary native source files with string manipulation. The name is honest: string-patching AppDelegate or MainApplication against whatever the current template looks like breaks silently when the template changes in a new SDK.

My line: structured helpers freely, regex-based source patching only when there is genuinely no alternative, and each dangerous mod documented with what it touches and why. If I find myself needing extensive raw native-source surgery, that's usually the signal to either write a proper native module with its own clean integration, or to question whether the dependency forcing it belongs in the project at all.

Debugging plugins when prebuild output is wrong

Plugin debugging is straightforward once you treat the generated projects as inspectable output. Run npx expo prebuild --clean to regenerate from scratch, then read the actual produced files — open the generated Info.plist, AndroidManifest.xml, or build.gradle and confirm your change landed where you expected. Diffing generated output before and after a plugin change is the fastest way to verify behavior, and npx expo config --type introspect shows the resolved config with all plugins applied without writing files.

Most plugin bugs I encounter are ordering or overwriting issues: two plugins touching the same file, with the later one clobbering the earlier one's changes. Plugins run in array order, so reordering in app.json is often the whole fix. When a third-party plugin misbehaves, reading its source is usually quicker than searching for issues — plugins are short, plain functions.

When to hire senior help

Senior help pays off when deciding between managed and prebuild workflows, setting up EAS-based CI/CD and OTA update channels, or untangling a project that was ejected prematurely. An experienced Expo engineer can usually configure build, submit, and update pipelines in days, a task that costs first-time teams weeks of trial and error. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.

Bottom line

Dhairya Senjaliya ships Mobile — Expo Development projects worldwide — book a scoping call to discuss your specific situation.

Common pitfalls to avoid

  • Ejecting to the bare workflow at the first native requirement instead of using config plugins and development builds, permanently giving up managed upgrades
  • Testing only in Expo Go and misconfiguring runtime versions for expo-updates, so an OTA update ships JavaScript that crashes against mismatched native binaries
  • Burning EAS build minutes on every commit in CI without caching or local builds, turning a convenience service into a surprise line item
  • Skipping several SDK upgrades in a row, then being forced into a large breaking migration when app store target API level deadlines arrive

Frequently asked questions

Do config plugins mean Expo can use any native SDK?

Nearly. Config plugins can apply any build-time native configuration — manifest entries, plist values, gradle settings, entitlements — which covers integrating the vast majority of native SDKs. What plugins don't do is write runtime native code; if an SDK needs custom native logic with no React Native wrapper, you'd write a native module (Expo Modules API makes this pleasant) alongside a plugin for its configuration.

Why did my manual edits to the ios or android folder disappear in Expo?

Because prebuild regenerates those directories from app.json and config plugins — they're build artifacts, not source. Any hand edit is erased on the next regeneration. Move the change into a config plugin using helpers like withInfoPlist or withAndroidManifest, or, if you intend to hand-maintain native projects permanently, stop running prebuild and treat the project as bare React Native.

Do I need to rebuild my app after adding a config plugin?

Yes. Config plugins affect the generated native project, so their changes only exist in a new binary — run prebuild and create a fresh development build (locally or via EAS Build). They cannot be delivered through over-the-air updates, which only carry JavaScript and assets. This is the key workflow distinction: JS changes iterate instantly, plugin and native-dependency changes require a build.

Should we use Expo or plain React Native for a new app?

The React Native docs themselves now recommend starting with a framework, and Expo is the primary one; 71% of surveyed developers already build with EAS. Expo today supports custom native code through development builds and config plugins, so the old limitations that forced teams to avoid it mostly no longer apply.

Does Expo lock us into their platform?

No; expo prebuild can generate standard native iOS and Android projects at any time, and the open-source tooling works without Expo's paid EAS services, as demonstrated by Doctolib running Expo without EAS on an app with 90 million users. EAS Build, Submit, and Updates are optional conveniences, not requirements.

Is Expo production-ready for a serious commercial app?

Yes; Expo Go is only a development sandbox, while production apps ship as normal store binaries built with EAS or locally. Plan for the SDK release cadence of roughly three versions per year, since staying current is required to keep up with store policy and React Native releases.

Bottom line: Dhairya Senjaliya ships Mobile — Expo Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership