Home / Articles / Treating Native Folders as Build Output with Expo Prebuild and CNG

This article is published in English.

Treating Native Folders as Build Output with Expo Prebuild and CNG

How Continuous Native Generation lets an Expo app use custom native modules, config plugins and EAS secrets without committing or hand-editing ios and android folders.

1103 words

For years, React Native teams on Expo faced the same fork in the road: the moment a project needed a custom native module, background audio or a vendor SDK, the answer was expo eject, and the tidy JavaScript project suddenly owned full ios and android directories. From then on, the team was also maintaining CocoaPods, editing build.gradle and debugging Xcode failures. Continuous Native Generation (CNG) and Expo Prebuild remove that trade-off. This guide explains how the model works, how config plugins replace manual native edits, how to recover from broken local Android builds, and how to feed secrets into cloud builds without committing them.

Native code as a build artifact

CNG rests on one principle: the native projects are something you generate, not something you maintain by hand.

Running npx expo prebuild produces the ios and android folders from a single source of truth, your app.json or app.config.js. Prebuild reads that configuration, applies the native changes it describes and emits complete, buildable native projects.

Because those folders can be recreated at any time, many teams add /ios and /android to .gitignore. When a native dependency ends up in a broken state, or when you upgrade React Native, you throw the folders away and regenerate them. The day-to-day question changes from "what did someone change in the Xcode project?" to "what does the config declare?".

That also sets the one rule of the model: once native folders are generated, do not hand-edit them. Any manual change disappears on the next prebuild. If you need a change, it has to be expressed in configuration.

Config plugins replace manual native edits

That raises an obvious question. How do you add a permission to AndroidManifest.xml or adjust AppDelegate.mm if the generated files are off limits?

Config plugins are the answer. A config plugin is a JavaScript function that runs during prebuild and modifies the native project in a controlled, repeatable way. Apps with demanding native requirements, such as real-time speech processing or rendering 3D models, depend on libraries that need deep native hooks, and those libraries typically ship their own plugins.

Instead of editing native code, you list the plugin and its options in app.json. The example below uses expo-build-properties to set the Android compileSdkVersion to 34, a value that would otherwise live in a Gradle file:

{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "android": {
            "compileSdkVersion": 34
          }
        }
      ]
    ]
  }
}

On the next prebuild, the plugin writes that setting into the generated Android project. Because the change is declared rather than applied by hand, it survives every regeneration and is visible in code review.

Keeping local Android builds healthy

Expo Go is excellent for early prototyping, but it only includes the native modules bundled with it. Once you rely on custom native code, you test with development builds, using npx expo run:android or npx expo run:ios. That matters especially for apps doing heavy on-device work, such as AI features, where you need to see real performance on a device or emulator.

The Android toolchain has a reputation for fragile caches. A typical scenario: you add a dependency, and the next local build fails with an obscure Java or Gradle error. The cause is usually stale build output rather than your code.

A clean build is the first thing to try. Go into the generated android directory, run Gradle's clean task to discard cached outputs, then return to the project root and build again:

cd android
./gradlew clean
cd ..
npx expo run:android

If a clean alone does not help, CNG gives you a stronger option: npx expo prebuild --clean deletes and regenerates the native folders entirely, which is safe precisely because nothing in them is hand-maintained. Building both habits into your routine saves long debugging sessions.

Supplying secrets to EAS cloud builds

CNG pays off most when combined with EAS (Expo Application Services) for cloud builds.

Take error monitoring. Production apps need it, and integrating Sentry means uploading source maps, which requires a SENTRY_AUTH_TOKEN. Traditionally, that token had to reach every build environment while staying out of the repository, which was awkward to manage.

With CNG and EAS, you add Sentry's config plugin to app.json and never commit the token. Instead, you register it as an environment variable in EAS with the CLI, giving it secret visibility and project scope so it is available to builds but not displayed afterwards. Run the command once, substituting your real token for the placeholder value:

eas env:create --name SENTRY_AUTH_TOKEN --value your_token_here --visibility secret --scope projecteas env:create --name SENTRY_AUTH_TOKEN --value your_token_here --visibility secret --scope project

During a cloud build, EAS runs prebuild to generate the native projects, the Sentry plugin reads the secret from the environment, configures the native SDK and uploads the source maps. No native file and no token ever lands in version control. The exact flags of eas env:create can change between CLI releases, so check the current EAS documentation if the command is rejected.

When CNG needs extra care

The model is powerful, but a few situations need planning:

  • Libraries without plugins. If a native SDK has no config plugin, you may need to write a small local plugin yourself rather than editing generated files.
  • Existing brownfield projects. Apps with years of hand-edited native code cannot simply delete their folders; migrating means moving each customization into configuration first.
  • Team discipline. CNG only works if everyone treats ios and android as disposable. A single quick edit in Xcode will be lost silently on the next regeneration.

Wrapping up

Expo Prebuild and CNG make complex, production-grade React Native apps far more approachable by making the native layers disposable:

  • Declare native requirements in app.json or app.config.js and let prebuild generate the rest.
  • Use config plugins for every native change so it survives regeneration.
  • Clean Gradle caches, or regenerate with prebuild --clean, when local builds fail mysteriously.
  • Keep tokens such as SENTRY_AUTH_TOKEN in EAS environment variables, never in the repository.

With the native projects reduced to build output, the team's attention goes back to React Native code, fluid interfaces and product features.