Expo’s new CNG workflow (Continuous Native Generation) fundamentally changes how we work with the ios/ and android/ directories in React Native projects. Instead of editing native code manually, CNG treats these folders as generated artifacts — rebuilt on demand from configuration, dependencies, and plugins.
This unlocks a cleaner workflow: you focus on JS + config, while Expo regenerates your native project reliably and consistently.
What is Continuous Native Generation?
Continuous Native Generation (CNG) is Expo’s evolution of the managed workflow. Rather than maintaining Xcode and Gradle files by hand, CNG regenerates them using your app config, package dependencies, and Config Plugins.
Why CNG matters
- Clean upgrades across Expo SDK versions
- Less native boilerplate to maintain
- Clear separation of app logic vs platform scaffolding
- Native changes automated through plugins instead of manual edits
CNG doesn’t mean you’ll never touch native code — it ensures you touch it only when truly required, and encapsulate those changes cleanly in reusable plugins.
Automating Native Config with Plugins
In a typical RN app, adding something like Firebase Analytics means touching:
- Project and App Gradle file
- Podfile with the Firebase analytics dependency
AppDelegate/MainApplication- Copy Google credentials files in Android and iOS directories (
google-services.json&GoogleService.InfoPlist)
With Expo Config Plugins, these steps become automated — ensuring your native project is always correct whenever you run:
expo prebuild
Let’s build a plugin that wires up Firebase Analytics on both platforms.
Initial Structure
First, we’ll set up a folder for the config plugin and generate a package.json for it. You can run these commands right from your Expo project’s root.
mkdir -p plugins/expo-analytics-config-plugin/src
cd plugins/expo-analytics-config-plugin
npm init -y
TypeScript Config
Next, create a tsconfig.json file that defines your rootDir, compilerOptions, and other TypeScript configurations. You can copy the example snippet below.
{
"compilerOptions": {
"outDir": "build",
"rootDir": "src",
"module": "commonjs",
"target": "es2019",
"lib": ["es2019"],
"declaration": true,
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
Setup Plugin Types and Configuration
In your index.ts, declare the plugin options. These options will be used to pass the metadata required by the config plugin.
import { ConfigPlugin } from "@expo/config-plugins";
type Options = {
android?: {
googleServicesJsonPath?: string;
googleServicesVersion?: string;
analyticsCollectionEnabled?: boolean;
};
ios?: {
googleServicesPlistPath?: string;
automaticScreenReportingEnabled?: boolean;
};
};
Android Firebase SDK Setup
The project-level Gradle file (using Expo’s withProjectBuildGradle API) should include the Firebase classpath declaration, while the app-level Gradle file (using Expo’s withAppBuildGradle API) should add the Firebase Analytics dependency. This ensures Firebase resolves correctly on Android. You can add the following code snippet to your config plugin’s index.ts.
import { withProjectBuildGradle, withAppBuildGradle } from "@expo/config-plugins";
const withAndroidConfiguration: ConfigPlugin<Options> = (config, options) => {
config = withProjectBuildGradle(config, (config) => {
const version = options.android?.googleServicesVersion ?? "4.4.2";
if (!config.modResults.contents.includes("com.google.gms:google-services")) {
config.modResults.contents = config.modResults.contents.replace(
/dependencies\s*{\s*/,
`$& classpath("com.google.gms:google-services:${version}")\n`
);
}
return config;
});
config = withAppBuildGradle(config, (config) => {
if (!config.modResults.contents.includes("firebase-analytics-ktx")) {
config.modResults.contents = config.modResults.contents.replace(
/dependencies\s*{\s*/,
`$& implementation("com.google.firebase:firebase-analytics-ktx")\n`
);
}
return config;
});
return config;
};
iOS Firebase SDK Setup
On iOS, dependencies are managed through Podfiles. Using Expo’s withPodfile API, we can include the Firebase/Analytics dependency. Since the Firebase SDK needs to be initialised in the AppDelegate file, we’ll use Expo’s withAppDelegate API to handle that setup automatically.
import { withPodfile, withAppDelegate } from "@expo/config-plugins";
const withIosConfiguration: ConfigPlugin<Options> = (config) => {
config = withPodfile(config, (config) => {
if (!config.modResults.contents.includes("Firebase/Analytics")) {
config.modResults.contents += '\npod "Firebase/Analytics"\n';
}
return config;
});
config = withAppDelegate(config, (config) => {
const firebaseInit = config.modResults.language === "objc"
? "[FIRApp configure];"
: "FirebaseApp.configure()";
config.modResults.contents = config.modResults.contents.replace(
/didFinishLaunchingWithOptions.*\{\n/,
`$& ${firebaseInit}\n`
);
return config;
});
return config;
};
Copy Firebase Credentials
Finally, implement the logic to copy the google-services.json and GoogleService-Info.plist files. These files contain the credentials required to initialise the Firebase SDK on Android and iOS respectively.
import { withDangerousMod } from "@expo/config-plugins";
import fs from "fs";
import path from "path";
const withCredentialsCopy: ConfigPlugin<Options> = (config, options) => {
return withDangerousMod(config, ["android", async (config) => {
const src = options.android?.googleServicesJsonPath;
if (!src) return config;
const dest = path.join(config.modRequest.platformProjectRoot, "app", "google-services.json");
fs.copyFileSync(path.resolve(config.modRequest.projectRoot, src), dest);
return config;
}]);
};
Export Plugin
Finally, combine all these components into a single exported plugin. Use createRunOncePlugin to ensure it runs only once during the prebuild process, preventing duplicate configuration.
import { createRunOncePlugin } from "@expo/config-plugins";
const withFirebaseAnalytics: ConfigPlugin<Options> = (config, options) => {
config = withAndroidConfiguration(config, options);
config = withIosConfiguration(config, options);
config = withCredentialsCopy(config, options);
return config;
};
export default createRunOncePlugin(
withFirebaseAnalytics,
"expo-analytics-config-plugin",
"1.0.0"
);
Update app.config.ts
Now add the plugin to your app.config.ts file and configure it with your Firebase credentials. This is where you’ll specify the paths to your google-services.json and GoogleService-Info.plist files.
plugins: [
[
"./plugins/expo-analytics-config-plugin",
{
android: {
googleServicesJsonPath: "./credentials/android/google-services.json",
googleServicesVersion: "4.4.2",
analyticsCollectionEnabled: true
},
ios: {
googleServicesPlistPath: "./credentials/ios/GoogleService-Info.plist",
automaticScreenReportingEnabled: true
}
}
]
];
✅ Test It
Run the build command to compile your TypeScript plugin, then execute expo prebuild --clean to regenerate your native directories with the Firebase configuration applied.
Once complete, you should see the Firebase Analytics dependencies added on both platforms, the credentials copied to their respective locations, and the initialization code automatically injected into the AppDelegate.
cd plugins/expo-analytics-config-plugin
npm run build
cd ../..
npx expo prebuild --clean
This Firebase example is just the beginning — once you build one plugin, you start thinking in automation by default.
Full demo repo: https://github.com/androidguy30/Expo-Config-Demo