◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-07-24Advanced

Resolving App Config in Three Layers: Merging Defaults, User, and Remote With Bounded Overrides

A single type-safe layer that merges compiled defaults, user preferences, and remote config. So a broken remote value never takes your app down, each key gets its own override strength, plus schema validation and range clamping, built from a real production incident.

Rork595React Native240Config ManagementRemote Config10Architecture22

✦ Premium Article

Late one night I opened my own wallpaper app on my iPhone, and a full-screen ad appeared every time I swiped to the next image. Twice, three times, inside half a minute. My stomach dropped.

It wasn't a bug in the code. The interstitial cooldown value I kept in remote config had somehow been set to something tiny, and that value had shipped straight to the app. Worse, the same config was shared across all six of the wallpaper apps I maintain as an indie developer, so every one of them broke at once.

What this drove home for me was how dangerous the assumption "remote config is safe because I can fix it remotely" really is. Yes, you can push a fix quickly. But for the twenty-odd minutes it takes that fix to reach every device, the app keeps running, exposed. The place to defend was never the delivery side. It was the device receiving the value.

This article is about a design that merges three sources, compiled defaults, user preferences, and remote config, into one type-safe layer, so that a broken value can arrive without taking the app down. It's based on the code I put into all six apps after that night.

Start From the Fact That Config Has Three Sources

In most apps, config quietly ends up being three-layered whether you plan for it or not. Let me lay them out in order.

The first is the defaults compiled into the binary. They're always present, even offline. This is where the source of truth for types and initial values lives.

The second is user preferences. Stored locally on the device (AsyncStorage or MMKV), they represent choices the user made deliberately, things like prefetch count or theme.

The third is remote config. External input arriving from something like Firebase Remote Config, holding values the operator wants to change quickly for everyone, typically ad frequency or the timing of a review prompt.

The trouble is merging these three with a naive "last one wins" override. That night's incident happened precisely because remote could overwrite anything. Having three layers is unavoidable. What you must avoid is giving every layer the same strength.

Give Each Key Its Own Override Strength

The heart of the design is declaring, per value, who is allowed to override it and how far. I settled on three policies.

locked — nobody can override

A flag like which onboarding version to show breaks the experience if it flips by accident, so I mark it locked. Neither remote nor the user can touch it; it stays at its default. It's a safety valve, deliberately made immovable.

user — user preference wins

A pure preference like prefetch count gets the user policy. Remote stays out of it, and only the value the user chose overrides the default. It simply isn't a value the operator should be changing behind their back.

remote — allow overrides, but only within a range

A value the operator wants to tune, like the ad cooldown, gets the remote policy. But I don't trust it wholesale. Each key defines a min/max, and the incoming value is always clamped into that range. That 3-second value from the incident now arrives raised to a floor of 60 seconds.

✦

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 type-safe merge layer that resolves defaults, user prefs, and remote config with per-key override strength (locked / user / remote)
✦How to keep one broken remote value from cascading, per-key fallback to defaults plus min/max clamping, written out in full
✦The real config incident across six wallpaper apps and the operational calls it forced (how to set bounds, what to log)
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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-08-01
Every Experiment Kept Landing on the Same Devices: Hash Choice and Salt Position, Measured Across a Million IDs
On-device experiment assignment looked fine in isolation, then collapsed the moment two experiments ran side by side. Here is what one million IDs revealed about four hash functions, why the culprit was the concatenation order rather than hash quality, and the implementation I settled on, with the measured numbers.
⬡ Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
⬡ Dev Tools2026-06-22
When a Poisoned Cache Crashes Your App on Every Launch — Designing a Safe-Mode Boot Your Users Can Escape On Their Own
When a persisted cache goes bad and the app crashes at the same spot on every launch, the only option left to the user is to reinstall. This article designs a safe-mode boot for Expo (React Native): the app counts its own early crashes, confirms a launch only once it becomes interactive, and resets just the dangerous state in graduated steps.