skip to content

Native Config Plugins

Config plugins let a project without committed native folders still change Info.plist, AndroidManifest.xml and Gradle files at prebuild. Interviewers ask how you add a native SDK without ejecting.

part ofExpo (React Native)overview, primer and where to startread it →
on this pageshow

explore

questions

10

In an Expo project, what is a config plugin function, and how does a mod plugin like withInfoPlist change a native file?

level: juniorimportance: must knowfreq 42%

answer

  1. a function over the app config
  2. (config, props) => config, named with…
  3. registers a mod, runs later
  4. modResults in, config out
  5. prebuild, then a new native build

basics

~20 s

An Expo config plugin is a synchronous function that takes the app config and optional props and returns the config. A mod plugin like withInfoPlist registers an async callback that edits the parsed Info.plist during npx expo prebuild.

solid answer

~40 s

A config plugin is a `ConfigPlugin<Props>` from `expo/config-plugins`: `(config, props) => config`, conventionally named `withSomething`. It runs every time the app config is evaluated, so it stays synchronous and never touches files itself. To change a native file it calls a **mod plugin** such as `withInfoPlist`, `withEntitlementsPlist` or `withAndroidManifest`, which registers an async **mod** on the config. Mods run only in the syncing phase of `npx expo prebuild` (locally or in EAS Build's prebuild step): the compiler reads the file, hands the callback `config.modResults` (the plist or manifest parsed into a JS object) and `config.modRequest` (project paths, platform, mod name), then writes back whatever `modResults` the callback returns. The callback must return the config. Users see the change only after a new native build.

code

typescript · 12 lines
typescript
import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';

type Props = { region: string };

const withApiRegion: ConfigPlugin<Props> = (config, { region }) => {
  return withInfoPlist(config, (config) => {
    config.modResults.ApiRegion = region;
    return config;
  });
};

export default withApiRegion;

go deeper

for a junior

Recall the shape: a function from config to config, named with a with prefix, that uses mod plugins such as withInfoPlist to edit native files during prebuild.

for a middle

Explain the two phases: plugins run at config evaluation and only register mods, and the mod compiler later reads each file, runs the callbacks on modResults and writes the result.

for a senior

Show you keep config edits outside mods, prefer typed mod plugins over text edits, and know that a plugin change needs a new native build, never an OTA update.

for a principal

Frame config plugins as the contract that lets native folders stay generated: weigh how many plugins a team maintains against the upgrade cost each one adds per SDK release.

## What a config plugin is An Expo project that uses **Continuous Native Generation** does not keep hand-edited `ios/` and `android/` folders as its source of truth; `npx expo prebuild` generates them from a template plus the app config. A **config plugin** is how a project or a library says "and also change this native file" in a repeatable way. Technically a plugin is just a function with the type `ConfigPlugin<Props>` exported from `expo/config-plugins` (the `expo` package re-exports `@expo/config-plugins` under that path): - It takes the **app config** (`ExpoConfig`) and an optional second argument, the **props**. - It returns the (possibly modified) config. - It is **synchronous** and should be fast, because it runs whenever the config is evaluated, not only at prebuild. - By convention it is named `with<Feature>`, for example `withApiRegion`. - Props must be **static values** (strings, numbers, booleans, arrays, plain objects), because the app config must serialize to JSON. A plugin may change plain config fields directly (`config.ios.bundleIdentifier`), but it cannot open a native file from its body. For that it uses a mod plugin. ## Two phases: config evaluation and mod compilation The key mental model is that a plugin **registers** work and prebuild **performs** it later: 1. **Config evaluation.** Expo CLI loads `app.json` or `app.config.ts` and applies every plugin. A call such as `withInfoPlist(config, callback)` does not run the callback; it stores it on the non-serialized `config.mods.ios.infoPlist` chain. 2. **Mod compilation.** During the syncing phase of `npx expo prebuild`, the mod compiler adds a **base mod** for each file. The base mod reads the file from disk (or the template), runs the registered callbacks, checks that each returned a valid config, then writes the result back. 3. **Native build.** The edited project is compiled by Xcode or Gradle, locally or on EAS Build. Because mods only run in step 2, anything that should be visible outside prebuild (for example to `npx expo config` or to the manifest the dev server serves) belongs in the plugin body, not inside a mod. ## What a mod callback receives | Property | What it holds | |---|---| | `config.modResults` | The file's data, typed per mod: an `InfoPlist` object, an `AndroidManifest` object parsed with xml2js, a properties list, or a string | | `config.modRequest.projectRoot` | The project root, where `package.json` lives | | `config.modRequest.platformProjectRoot` | The `ios/` or `android/` folder | | `config.modRequest.platform` / `modName` | Which mod is running, for example `ios` and `infoPlist` | | `config.modRequest.projectName` | iOS only: the folder name under `ios/` | | `config.modRequest.introspect` | `true` when results are only being previewed, not written | The callback edits `config.modResults` and **returns `config`**. If it returns nothing, the compiler's sanity check throws a "not a valid project config" error naming the mod. ## The common mod plugins | Mod plugin | File | Data you edit | |---|---|---| | `withInfoPlist` | `ios/<name>/Info.plist` | JS object, merged with `ios.infoPlist` from app config | | `withEntitlementsPlist` | the app target's `.entitlements` | JS object, kept in sync with `ios.entitlements` | | `withAndroidManifest` | `android/app/src/main/AndroidManifest.xml` | JS object from xml2js | | `withGradleProperties` | `android/gradle.properties` | list of property items | | `withPodfileProperties` | `ios/Podfile.properties.json` | JSON | | `withDangerousMod` | any file, by path | nothing: you read and write the file yourself | The docs flag the string-based mods (`withAppDelegate`, `withMainApplication`, `withAppBuildGradle` and similar) and `withDangerousMod` as risky, because they rely on text matching. ## Composing plugins A feature usually needs an iOS half and an Android half. Write each as its own plugin function and compose them in a top-level plugin, either by calling them in sequence or with `withPlugins(config, [pluginA, [pluginB, props]])`, which applies a list of plugins in order. Libraries expose the top-level plugin from `app.plugin.js`; how a project lists and orders plugins is a separate topic. ## When the change reaches the app - A JavaScript reload re-runs only JS; it never re-runs mods. - The change appears after `npx expo prebuild` and a **new native build** (a development build or an EAS Build). - For the same reason an over-the-air update cannot deliver a config plugin change: it ships JavaScript and assets, not native project edits.

  • Why should an Expo config plugin change app-config fields such as ios.bundleIdentifier outside a mod rather than inside one?
    Mods run only in prebuild's syncing phase, while the plugin body runs on every config evaluation: `npx expo config`, the dev server's manifest, EAS reading the project. A config field set inside a mod is invisible to all of those consumers. So plain config changes go in the plugin body, and only native file edits go inside a mod callback.
  • Why must the props passed to an Expo config plugin be static values rather than functions or promises?
    The app config, including the plugins array and its props, has to serialize to JSON so it can be used as the app manifest. A function or promise cannot be serialized. Expo's guidance is to keep props to strings, numbers, booleans, arrays and plain objects, and to prefer good defaults over required options.
  • A developer adds a config plugin, reloads the app, and the new Info.plist key is missing; what did they skip?
    Reloading re-runs only JavaScript. The plugin edits native project files, so they must run `npx expo prebuild` (or let EAS Build do it) and install a new native build. The same reason means an over-the-air update cannot deliver the change.

saying these in an interview costs you the question

  • A config plugin runs on the device when the app starts.
  • Mod callbacks run every time Metro rebuilds the JavaScript bundle.
  • Mutating modResults is enough; the mod callback need not return config.
  • A config plugin change can reach users through an over-the-air update.
  • Plugin props can be functions because app.config.ts is JavaScript.
open as a page

In an Expo project, what does an entry in the app config's plugins array do, and when do its changes reach the installed app?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Each plugins entry names a config plugin, a Node.js function that edits the Expo config and registers native file changes. Those changes are written at prebuild, so they reach users only in a new native build, never through a JavaScript update.

open as a page

In an Expo project, how do you raise the Android minSdkVersion for a maps SDK without editing build.gradle, and where does the value land?

level: middleimportance: should knowfreq 38%

basics

~10 s

Add ["expo-build-properties", { "android": { "minSdkVersion": 26 } }] to plugins and re-run prebuild. The plugin writes android.minSdkVersion into android/gradle.properties, which the generated Gradle build reads.

open as a page

In an Expo app config, why does the order of the plugins array matter when two plugins edit the same native file?

level: middleimportance: should knowfreq 30%

basics

~20 s

Plugin functions run in array order, but each mod for a file wraps the earlier ones, so at prebuild the later plugin's edit runs first and the earlier plugin's edit runs last. For the same manifest key, the plugin listed earlier wins.

open as a page

How would you write an Expo config plugin that adds an iOS App Groups entitlement and an Android manifest meta-data entry idempotently?

level: middleimportance: should knowfreq 28%

basics

~10 s

Write two plugin functions and compose them: withEntitlementsPlist adds the group to the com.apple.security.application-groups array only if missing, and withAndroidManifest uses AndroidConfig.Manifest.addMetaDataItemToMainApplication, which updates an existing entry instead of duplicating it.

open as a page

In an Expo project, how do you see what your config plugin's mods will produce without generating the ios and android folders?

level: middleimportance: should knowfreq 16%

basics

~10 s

Run npx expo config --type introspect: it evaluates the introspectable mods, such as Info.plist, entitlements and the Android manifest, in memory and prints their results under _internal.modResults without writing any native files.

open as a page

In an Expo project, a config plugin option was changed but the next build still has the old native value; what are the likely causes, and how do you fix it?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The change never passed through prebuild: native folders were committed so EAS Build skipped prebuild, stale local folders were reused, or prebuild ran without --clean. Run npx expo prebuild --clean, check the file, and rebuild.

open as a page

When an Expo app config lists a library name in plugins, how does Expo find that library's config plugin, and why do libraries ship app.plugin.js?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

A path loads as-is; a package name resolves to app.plugin.js (or .cjs, .mjs, .ts, .cts, .mts) at the package root, else the package's main entry. app.plugin.js keeps Node-run config code separate from the runtime code Metro bundles.

open as a page

In Expo config plugins, what does createRunOncePlugin do, and why do libraries wrap their exported plugin with it?

level: middleimportance: nice to knowfreq 11%

basics

~20 s

createRunOncePlugin wraps a plugin with withRunOnce: if its name is already in the config's _internal.pluginHistory it returns the config unchanged, otherwise it records the name and version and runs. Libraries use it so their plugin never applies twice.

open as a page

An Expo config plugin uses withDangerousMod to regex-insert a line into ios/Podfile, and each prebuild without --clean adds another copy; why, and how do you fix it?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Without --clean, prebuild edits the Podfile already on disk, and the dangerous mod re-inserts its line because nothing checks it is present. Fix it with a guarded, tagged insert or, better, a non-dangerous mod such as withPodfileProperties.

open as a page