◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/App Dev
◇ App Dev/2026-08-24Advanced

I Moved the Thumbnails Assuming the Shipped Build Would Keep Asking for the Old Path

Reorganizing image directories on the server does not reach the builds already installed on people's phones. Here is the two-way fallback that returns 200 for both the old and new paths, the ordering rule for images versus catalog, and the four checks I run through a CDN before calling a release live.

asset deliverywallpaper apps3Expo213operations9CDN

✦ Premium Article

The new 6.5-inch thumbnails were generated, verified, and moved into the master tree. All that was left was the upload. That is where I stopped.

Renaming the directory meant that the URL the currently shipped iOS build assembles and the place where the files would actually live from tomorrow were about to disagree. On my machine the simulator runs the newest code, so this disagreement never surfaces during development. It only exists on other people's phones.

Running wallpaper apps as an indie developer for as long as I have, this kind of "server-side only" cleanup comes up regularly. You cannot push a build through App Store review every time you add a single image, so the actual content lives on the server. The price of that freedom is that every directory you rename collides with an assumption some shipped build baked in months ago.

The move itself takes ten minutes. The hard part was deciding what evidence would let me say the release had actually landed.

What a shipped build holds is not the image — it is the rule that builds the URL

An app that delivers its content remotely does not carry the images. It carries a function from ID to URL.

// The rule the currently shipped build (iOS 4.1.1) carries
const STORAGE_BASE = "https://storage.example.net";
const THUMB_DIR = "5_8inch/thumb_full";   // frozen at build time
 
export function thumbUrl(app: string, id: number) {
  return `${STORAGE_BASE}/ios/wallpaper_apps/${app}/${THUMB_DIR}/${id}.jpg`;
}

THUMB_DIR will not change by a single character until that device installs a newer build. The moment the files move to 6_5inch/thumb_full/, every shipped build keeps knocking on a door that is no longer there.

The part that is easy to miss: this rule differs per version, not per app. Across my wallpaper apps the iOS and Android lines are not in sync — iOS is mostly on 4.1.1 while Android sits on 1.9.0, and each shipped with its own directory convention. The blast radius of a move is not "how many apps do I have"; it is "how many distinct versions are still in active use."

Open the version breakdown in the store console and you will usually find a build from six months ago still holding a double-digit share. People who turned off automatic updates. Devices on metered connections. Phones restored from an old backup after an upgrade. The old path does not disappear the day after you finish moving files.

Remote config only helps from the build that contains it

My first instinct was to make THUMB_DIR come from remote config.

type RemoteConfig = { thumbDir?: string };
 
export async function loadThumbDir(): Promise<string> {
  try {
    const res = await fetch(`${API_BASE}/ios/wallpaper_apps/${app}/config.json`, {
      cache: "no-store",
    });
    const cfg = (await res.json()) as RemoteConfig;
    return cfg.thumbDir ?? "6_5inch/thumb_full";   // current default
  } catch {
    return "6_5inch/thumb_full";                    // keep building URLs offline
  }
}

Clean enough. But it rescues exactly one population: builds shipped after the change. The builds giving me trouble are the ones without this branch.

That was my own mistake, stated plainly. Remote config buys future flexibility; it does not travel backwards into binaries you already shipped. The only thing that can reach those binaries is the response your server sends.

For the same reason I stopped hard-coding fallback constants such as the catalog total. When a fallback literal drifts away from the real value, the devices that failed to reach the network are the only ones living in the stale world — which is precisely the population you cannot debug. Those responses are now assembled from the same variable the live endpoint uses.

✦

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
✦You will be able to inventory what your already-shipped builds assume, and decide for yourself when it is safe to start reorganizing anything on the server
✦You will be able to move assets while old-path traffic is still arriving, without users ever seeing a screen full of missing thumbnails
✦You will be able to tell within minutes whether a release is half-applied — images uploaded, catalog still stale — even with a CDN sitting in front of everything
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 $15 for lifetime access
View Membership →

Related Articles

◇ App Dev2026-09-23
It Crashed, but the Reviews Say "It Freezes" — When an Android Crash Leaves a Blank Screen Behind
Your crash reports show exceptions, yet users only ever say the app freezes. Here is why expo-updates plus the New Architecture can leave an empty Android screen behind, and the order I now use: a way out first, a bad-exit marker second, root cause last.
◇ App Dev2026-09-18
The three lines I check when prebuild stops on the standard SDK 57 Swift AppDelegate error
Turning on ios.enableSceneSupport for iOS 27 can stop prebuild cold. The cause was not my SDK version but the shape of AppDelegate.swift. Here are the three lines to check, and why copying the SDK 58 migration steps by hand will break your build.
◇ App Dev2026-09-16
The Token I Deleted Came Back After a Restart — Detecting Failed SecureStore Deletes
On Android, getItemAsync can report null right after sign-out while the value is still sitting on disk. Here is how I rewrote a sign-out path that was verifying deletion by reading instead of by the delete result.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links