AWS Builder Center
Implementing OTA Updates using Hot Updater + Amazon S3, CloudFront and Lambda@edge

Implementing OTA Updates using Hot Updater + Amazon S3, CloudFront and Lambda@edge

Over-The-Air (OTA) updates are a quick and easy way to push Mobile App changes like frontend edits and bug fixes directly to users’ devices, over the air, without going through the Play Store or Apple App Store.

Infrastructure | DevOps | Blockchain
I once conducted a Well Architected review for a client whose infrastructure was on AWS. The final scope of the project was to implement an OTA update deployment for their mobile application.
At the time, I had never heard of OTA before. So naturally, that led to a few hours of research, reading, and mild confusion, until I finally understood that Over-The-Air (OTA) updates are a quick and easy way to push Mobile App changes like frontend edits and bug fixes directly to users’ devices, over the air, without going through the Play Store or Apple App Store.
OTA — Over The Air.
I’m assuming you get it now 😉.
One thing you should have learned by now in our field is that we never mean these names literally. Just like the Cloud isn’t actually somewhere up in the sky, OTA updates don’t literally pass “over the air” either. (Disappointing, I know.)
After this realization, my immediate thought was: Wait… isn’t this more of a Mobile Developer problem than a Cloud Engineer one?
So I asked the client a few more questions to really understand where I come in. That’s when their use case finally clicked.
For the OTA updates, we were going to use a tool called Hot Updater. I’ll explain what that does later as we go along.
OTA Updates Architecture Flow
Here’s how the OTA workflow actually works.
A developer fixes a bug in the JavaScript code.
They run a build and generate a new OTA update bundle. At this point, the bundle is just files — JavaScript and assets. Nothing magical yet.
Those files are uploaded to Amazon S3.
In front of S3 sits CloudFront, which you can think of as a fast, global delivery system. From this point on, any device that needs the update will download it through CloudFront.
The developer also updates a set of simple rules. These rules answer questions like:
  • Which app versions can receive this update?
  • Which platform is it for?
  • Is this a full rollout or just a small test?
These rules are used by Lambda@Edge.
Now, a user opens the app.
The app’s Hot Updater wakes up and asks:
“Hey… is there an update for me?”
That request goes to CloudFront.
Before CloudFront responds, Lambda@Edge jumps in.
Lambda@Edge looks at the request and thinks:
  • What app version is this?
  • iOS or Android?
  • Is this user part of the rollout?
Based on that, it decides:
  • “Yes, this device should get the new update.”
  • Or, “Nope. Stay on the current one for now.”
If the answer is yes, Lambda@Edge points CloudFront to the correct update files in S3.
CloudFront delivers those files quickly to the app.
Hot Updater downloads the update, verifies it, applies it, and reloads the app.
The user sees the bug fixed — without updating the app from the store.
And if something goes wrong?
The developer simply changes the rules.
Lambda@Edge immediately starts sending users back to the previous update.
No redeploy.
No app store review.
Just instant rollback.
This article isn’t meant to be a step-by-step guide on how to implement OTA Updates. You can find that in the official Hot Updater documentation, and it’s actually pretty straightforward.
I’m just here to share the blockers I ran into during the implementation and how I worked through them.
So… let’s dive right in.

The Blocker That Almost Made Me Question My Life Choices

Everything looked correct.
The OTA workflow was set up.
The JavaScript changes were clean.
No native code had been touched.
And yet…
CI kept screaming: “Native changes detected.”
Every. Single. Time.
Even for JavaScript-only updates.
At first, this felt like one of those problems where you reread the logs ten times, hoping the error message magically changes. It didn’t.

The Root Cause (a.k.a. Paths Will Betray You)

The issue came down to the fingerprint system, specifically, the fingerprint.json file.
This file contained absolute file paths, and those paths were different depending on where the fingerprint was generated.
For example:
  • Local machine
1
/Users/dev-user/Documents/company-mobile/node_modules/...
  • CI (GitHub Actions)
1
/Users/runner/work/company-mobile/company-mobile/node_modules/...
Same code.
Same dependencies.
Different paths.
And those differences were enough to produce different hashes.
So the system assumed:
“Oh, something in native land must have changed.”
Even when it absolutely hadn’t.
The main offender here was rncoreAutolinkingConfig, which quietly embedded environment-specific paths into the fingerprint.

How This Broke Everything in Two Very Annoying Ways

Issue 1: CI Always Failed (Even When Nothing Changed)

  • JavaScript-only changes still triggered fingerprint mismatches
  • The workflow constantly reported “native changes detected”
  • OTA updates were blocked for no good reason
At this point, OTA had basically lost its “quick” advantage.

Issue 2: Store Builds and OTA Updates Were Speaking Different Languages

This one was worse.
Here’s what was happening:
Store build (done locally):
1
2
3
4
5
6
7
Developer builds app locally
↓
Local fingerprint generated (with local paths)
↓
App uploaded to App Store / Play Store
↓
Native hash = "ABC123"
OTA deployment (done on CI):
1
2
3
4
5
CI generates fingerprint (with CI paths)
↓
OTA bundle deployed
↓
Expected native hash
= "XYZ789"
What the app sees:
1
2
3
App native code hash: ABC123
OTA server expects: XYZ789
❌ Update rejected
So even though the code was the same, the hashes didn’t agree.
Which meant:
  • OTA updates failed
  • Rollbacks didn’t behave correctly
  • Everyone was confused

The Fix (A Two-Part Truce Between Local Machines and CI)

Part 1: Let CI Take Ownership of the Fingerprint (One-Time Fix)

The first rule we established was simple:
CI is the single source of truth for fingerprints.
Here’s what we did:
  • Deleted the existing fingerprint.json (it was polluted with local paths)
  • Forced CI to regenerate it
  • Committed the new fingerprint back into the repository
Temporary CI workflow step:
1
2
3
4
- name: Force regenerate fingerprint
run: |
rm -f fingerprint.json
yarn hot-updater fingerprint create
1
2
3
4
5
- name: Commit new fingerprint
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: regenerate fingerprint with CI paths"
file_pattern: "fingerprint.json"
Result:
  • fingerprint.json now contains CI-based paths
  • Future CI runs stopped detecting fake native changes
  • Workflow returned to normal fingerprint checks
This fixed CI… but local store builds were still a problem.

Part 2: Making Local Store Builds Respect CI Fingerprints (Permanent Fix)

The Problem

Developers were building locally for the App Store and Play Store.
That was fine, except local builds were generating new fingerprints, undoing everything CI had just fixed.
What we needed was a way for local builds to:
  • Read CI’s fingerprint
  • Reuse its hashes
  • Never generate new ones

The Solution: Sync, Don’t Generate

We created a small system that reads**** fingerprint.json**** from git and injects the hashes into native configs before building.

Files Created

  1. scripts/get-fingerprint.js – Reads hashes from fingerprint.json
  2. scripts/update-android-fingerprint.js – Updates Android strings.xml
  3. app.config.js – Uses Expo’s native infoPlist for iOS

How It Works

iOS (Automatic via Expo)
1
2
3
4
5
6
7
8
9
Developer runs: yarn prebuild:ios
↓
expo prebuild reads app.config.js
↓
app.config.js calls getFingerprintHashes()
↓
Reads fingerprint.json (from CI)
↓
Writes hash to Info.plist ✅
Android (Script-Based)
1
2
3
4
5
6
7
8
9
Developer runs: yarn prebuild:android
↓
expo prebuild generates files
↓
Script runs: node scripts/update-android-fingerprint.js
↓
Reads fingerprint.json (from CI)
↓
Updates strings.xml with hash ✅
package.json scripts:
1
2
3
4
{
"prebuild:ios": "npx expo prebuild --platform ios",
"prebuild:android": "npx expo prebuild --platform android && node scripts/update-android-fingerprint.js"
}

The Final Workflow (a.k.a. Peace Restored)

For Developers (Store Builds)
1
2
3
4
5
git pull origin staging-ota # Get CI fingerprint
yarn build:ios
yarn build:android
# Upload to stores
git push
For CI (OTA Deployments)
1
2
3
4
# No change needed
- Check fingerprint
- JS-only changes → Deploy OTA
- Native changes → Update fingerprint → Deploy OTA

Rules We Now Live By

✅ DO

  • Always git pull before building for stores
  • Let CI create and update fingerprints
  • Use the provided build scripts

❌ DON’T

  • Run yarn hot-updater fingerprint create locally
  • Manually edit fingerprint.json
  • Build store apps without syncing latest fingerprints

What Actually Fixed It

  • CI owns the fingerprint → one source of truth
  • Local builds sync from CI → no more mismatches
  • Automation over discipline → scripts, not memory
  • Removed problematic plugin → used Expo native config + scripts instead

The Result

  • ✅ JavaScript changes → OTA deploys instantly
  • ✅ Native changes → CI updates fingerprint correctly
  • ✅ Store builds → Perfect match with OTA updates
  • ✅ No more silent rejections
And most importantly…
OTA started behaving like OTA again.

Final Thoughts

If there’s one thing this whole experience taught me, it’s that OTA updates aren’t hard*;* they’re just unforgiving. Everything can look correct on the surface, while a tiny detail (like an absolute file path) quietly breaks the entire system.
This wasn’t a Hot Updater problem.
It wasn’t an AWS problem.
It was a consistency problem.
Once we stopped letting every environment “do its own thing” and gave CI full ownership of the fingerprint, everything finally fell into place. OTA updates became fast again. Rollbacks worked. And JavaScript fixes went back to feeling like magic instead of a gamble.
If you’re ever implementing OTA updates using Hot Updater + AWS (S3, CloudFront, and Lambda@Edge) and things start behaving… suspiciously, I hope this article saves you a few hours of log-staring and quiet frustration.
Save it. Bookmark it. Future-you might need it.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article