●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Managing Native Settings Across Rork-Exported Apps with a Custom Expo Config Plugin
Tired of re-typing your AdMob ID and ATT string every time prebuild wipes your Info.plist? Here is how I made native settings reproducible with a custom Expo config plugin and shared it across six wallpaper apps.
The week after I wired AdMob into an app exported from Rork, I added another library and ran npx expo prebuild. The GADApplicationIdentifier I had hand-written into Info.plist the previous week was simply gone. Of course it was — the whole ios/ folder gets regenerated. But I didn't know that at the time, so I wrote it back, watched it vanish, and repeated that a few times.
I have been building apps solo since 2014, and the wallpaper apps that grew to 50 million cumulative downloads now run as six parallel titles. Typing the same AdMob bootstrap by hand six times is nothing but a breeding ground for mistakes. I once pasted the wrong App ID into a single app and shipped a release where no ads showed at all. A custom Expo config plugin is what finally removed this "hand-edits that linger in the native layer" problem at the root.
Why your native settings vanish on every prebuild
The whole appeal of Rork and Expo is that you rarely think about ios/ and android/. Those native folders are output, not source. From the declarations in app.config.ts and package.json, npx expo prebuild regenerates the native projects from scratch every time.
Miss this and you will struggle endlessly. Editing the generated ios/MyApp/Info.plist by hand is like writing in sand — the next prebuild washes it away. If you keep ios/ and android/ in .gitignore for a more managed workflow, this is doubly true.
The right mental model is that native settings should also be generated from declarations. A config plugin is the bridge between that declaration and the generated output. Both of my grandfathers were temple carpenters, and growing up watching them cut wood exactly to the drawing left me with a deep preference for state that can be reproduced from the same procedure. A config plugin gives native settings exactly that reproducibility.
How a config plugin works inside prebuild
A config plugin is a function that rewrites native files midway through prebuild, using a set of "mods" provided by @expo/config-plugins. The common ones are:
withInfoPlist — receives the iOS Info.plist as a JS object and lets you mutate it
withEntitlementsPlist — handles iOS entitlements such as Associated Domains
withAndroidManifest — exposes AndroidManifest.xml as a parsed object
withAppBuildGradle — edits the app/build.gradle string directly
withDangerousMod — a last resort for touching any file directly
Prebuild applies these mods in order, then writes out the native project. So instead of editing Info.plist by hand, you write a JS instruction that says "always put this value in, every time you prebuild."
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A complete config plugin that injects your AdMob App ID, ATT string, and SKAdNetwork list during prebuild
✦A shared-module setup so one edit propagates native settings to all six apps at once
✦Real production gotchas: withDangerousMod idempotency, mod ordering, and why plugins never run on EAS Update
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Run npx expo prebuild --clean and the generated ios/WallpaperApp/Info.plist now contains NSAppGreeting — without you ever opening the file. That is the "native settings generated from declarations" feeling in miniature.
A plugin that injects AdMob and ATT together
Now the real work. Every wallpaper app needs the AdMob App ID, the ATT (App Tracking Transparency) description, and the SKAdNetwork identifier list. Let's consolidate them into one plugin.
The Before workflow was a manual edit after each prebuild:
# Manual work after every prebuild (destined to disappear)1. Open ios/WallpaperApp/Info.plist2. Paste GADApplicationIdentifier3. Write NSUserTrackingUsageDescription4. Paste the SKAdNetworkItems list, endlessly5. -> next prebuild wipes it all -> back to 1
After, that entire procedure moves into the plugin.
// plugins/withAdConfig.jsconst { withInfoPlist } = require("@expo/config-plugins");// Frequently used SKAdNetwork IDs (grow this to match your ad SDK)const SKADNETWORK_IDS = [ "cstr6suwn9.skadnetwork", "4fzdc2evr5.skadnetwork", "v9wttpbfk9.skadnetwork",];module.exports = function withAdConfig(config, props) { const { admobAppId, trackingMessage } = props; return withInfoPlist(config, (config) => { const plist = config.modResults; // 1) AdMob App ID plist.GADApplicationIdentifier = admobAppId; // 2) ATT description (localize separately via InfoPlist.strings) plist.NSUserTrackingUsageDescription = trackingMessage; // 3) Build the SKAdNetwork list idempotently plist.SKAdNetworkItems = SKADNETWORK_IDS.map((id) => ({ SKAdNetworkIdentifier: id, })); return config; });};
The caller passes props as the second element of the array.
// app.config.tsexport default { expo: { name: "WallpaperApp", plugins: [ [ "./plugins/withAdConfig.js", { admobAppId: process.env.ADMOB_IOS_APP_ID, trackingMessage: "Used to show wallpapers and recommendations tailored to you.", }, ], ], },};
The key point is reading the App ID from process.env. You never hard-code the value, so the same plugin works across apps simply by switching .env. Having once shipped a release with no ads because of a mispasted ID, I strongly recommend defining each ID in exactly one place.
Sharing it across six apps
Copying withAdConfig.js into all six repositories just multiplies the number of places you have to edit. The SKAdNetwork list changes from time to time as ad SDKs update, so keeping six copies in sync by hand is not realistic.
I moved it into a shared package. The procedure is three steps:
Create a small npm package @dolice/native-config (or an internal monorepo package) holding withAdConfig.js and the SKAdNetwork list
Add it as a dependency in each app's package.json
Reference it by package name from app.config.ts
// Each app's app.config.ts (identical across all six)export default { expo: { name: "WallpaperApp", plugins: [ [ "@dolice/native-config/withAdConfig", { admobAppId: process.env.ADMOB_IOS_APP_ID, admobAndroidId: process.env.ADMOB_ANDROID_APP_ID, trackingMessage: process.env.ATT_MESSAGE, }, ], ], },};
Now when the SKAdNetwork list changes, you edit exactly one file in the package. Each app just bumps the dependency version and prebuilds, and the same change lands in all six. App-specific values (App ID, localized strings) stay in .env, so shared code and per-app settings remain cleanly separated.
Covering Android with the same plugin
The same philosophy applies to Android's AndroidManifest.xml. AdMob's App ID has to go into a manifest <meta-data> entry. Using withAndroidManifest, the one plugin function handles both platforms.
// plugins/withAdConfig.js (Android part added)const { withInfoPlist, withAndroidManifest, AndroidConfig,} = require("@expo/config-plugins");function withAndroidAdConfig(config, props) { return withAndroidManifest(config, (config) => { const app = AndroidConfig.Manifest.getMainApplicationOrThrow( config.modResults ); AndroidConfig.Manifest.addMetaDataItemToMainApplication( app, "com.google.android.gms.ads.APPLICATION_ID", props.admobAndroidId ); return config; });}module.exports = function withAdConfig(config, props) { config = withIosAdConfig(config, props); // the iOS part, extracted into a function config = withAndroidAdConfig(config, props); return config;};
The AndroidConfig.Manifest helpers spare you from editing XML as raw strings. addMetaDataItemToMainApplication overwrites a same-named key, so no matter how many times you prebuild, the <meta-data> never duplicates. That idempotency pays off later.
Production gotchas I hit
Even after the plugin was running, a few things tripped me up in production. I'll leave them here as notes.
First, plugins only run during prebuild. EAS Update (OTA) does not change native settings, so even if you "fixed" the App ID, nothing changes until you build and ship a new binary. I lost a couple of hours wondering why my fix wasn't taking. The cure is to remember that native changes always require a build.
Second, a plugin written in TypeScript can't be loaded as-is. app.config.ts itself is fine, but the plugin referenced from the plugins array must be CommonJS .js (or precompiled). I settled on writing plugins plainly in .js.
Third, mod ordering matters when several mods touch the same file. Register two withInfoPlist mods and the later one may overwrite the earlier. Consolidate processing for a single plist into one mod.
Fourth, when you use withDangerousMod, you must guarantee idempotency yourself. If you append to a file, the line count grows on every prebuild. Always check "does this line already exist" before appending. Where possible I recommend avoiding withDangerousMod and using structured mods like withInfoPlist.
Finally, when a change isn't taking, npx expo prebuild --clean rebuilds ios/ and android/ and makes diagnosis far easier. Stale output can hide your plugin changes.
Your next step
Start by writing the smallest plugin — one that adds a single key to Info.plist — in a Rork project you have on hand, then prebuild. Once you can see that value in the generated plist with your own eyes, you've internalized the feeling of reproducing native settings from source. From there you can move every hand-edited setting, AdMob and ATT alike, into the plugin one at a time.
I braced myself at first, treating native as a black box, but going through config plugins brought me close to the carpenter's drawing: the same input always builds the same house. If you are juggling multiple apps too, I hope this helps you out of settings hell.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.