Beyond EAS: Engineering a Custom, Self-Hosted OTA Update Engine for Enterprise Expo Apps
A deep dive into bypassing EAS Update to design and engineer a private, secure, and self-hosted Over-The-Air (OTA) update infrastructure. Learn how to meet strict compliance guidelines, air-gapped constraints, and high-security requirements.

Don’t get me wrong: Expo Application Services (EAS) is a phenomenal suite of tools. For 90% of teams, EAS Update is the gold standard for shipping hotfixes and minor features without waiting on App Store or Google Play reviews.
But when you work in highly regulated environments—think defense, banking, or healthcare—the story changes.
In my journey architecting enterprise mobile infrastructures, I’ve repeatedly hit the compliance wall. Strict data residency laws, the requirement for air-gapped environments (where devices live on private, isolated intranets), and SOC2 Type II boundaries often dictate that no intellectual property (i.e., your compiled JS bundles) can traverse or reside on third-party cloud infrastructure.
If you find yourself in this boat, you don't have to abandon Expo. Instead, you can build your own.
In this post, I will share how we engineered a secure, high-throughput, self-hosted Over-The-Air (OTA) update system designed specifically for enterprise Expo applications, bypassing EAS completely.
Demystifying the Expo Updates Protocol (v1)
To build a drop-in replacement for EAS Update, we first have to understand the communication protocol between the native client (expo-updates) and the update server.
Expo's runtime expects a specific handshake. When your application boots up, the expo-updates native module intercepts the launch sequence and makes a request to the endpoint configured in your app.json under updates.url.
The Request Headers
Your self-hosted endpoint will receive a GET request containing several vital headers:
expo-protocol-version: Currently1. This defines the structured header and multipart response format.expo-runtime-version: The native runtime version (e.g.,1.0.0or an SDK fingerprint). This ensures you don't push a bundle that relies on native modules not compiled into the current binary.expo-platform:iosorandroid.expo-expect-signature: Requests a cryptographic signature to verify the integrity of the manifest.
The Response Structure
Your server must respond with a multipart mixed response or a highly specific JSON structure if using Protocol v1. The response delivers a Manifest—a blueprint containing metadata about the update and pointing to the location of the assets (JS bundles, images, fonts).
Here is a simplified architectural look at the custom deployment pipeline:
+-------------------------+ 1. npx expo export +------------------------+
| CI/CD Build Runner | ---------------------------> | Private S3/MinIO |
| (Custom Signing & Prep) | | (Bundles & static assets) |
+-------------------------+
| ^
| 2. Register Metadata |
v | 3. Download Assets
+-------------------------+ 4. Fetch Signed Manifest +------------------------+
| Custom Update API | <----------------------------------- | Client App (iOS/And) |
| (Node/NestJS + Postgres)| | `expo-updates` Native |
+-------------------------+ +------------------------+
The Critical Security Layer: Code Signing
In a self-hosted architecture, security is your absolute priority. Because OTA updates execute dynamic JavaScript directly on user devices, a compromised update server is an existential threat.
To prevent Man-in-the-Middle (MITM) attacks and malicious bundle injections, we enforce rigorous cryptographic code signing. The Expo client native library natively supports verification of these signatures using a public/private key pair.
Step 1: Generate Key Pair
First, generate an RSA 2048-bit key pair on your secure build runner or local machine:
Convert your public key to a base64 string. This public key will be embedded directly in your app's binary via app.json:
Engineering the Update Server (NodeJS + Fastify)
Let’s build a lightweight, production-grade custom update server using TypeScript and Fastify. We'll handle incoming requests, validate runtime constraints, construct the update manifest, and cryptographically sign the response.
The Manifest Controller
The Automated CI/CD Pipeline
Now that our server is ready to listen, how do we compile and ship updates securely without touch-points from manual developers?
Instead of calling eas update, we drop down to the Expo CLI to generate a static export of the JS bundles and assets.
Here is a bash script that handles compiling, computing asset SHA-256 hashes, and preparing assets for upload to our internal object storage (MinIO or AWS S3):
Battle Scars: Hard-won Production Lessons
When we rolled this custom infrastructure out across thousands of enterprise tablets running inside private networks, we hit a few unexpected snags. Here is the operational advice I wish I had on Day 1:
1. Watch Out for Hermetic Bytecode Differences
If you have enabled Hermes (which you should), your JS code is pre-compiled into bytecode. Make sure that the version of Hermes used on your CI pipeline matches the native version compiled inside the targeted IPA/APK binaries exactly. A minor mismatch in the compiler version can crash your application immediately on boot when loading a dynamic bundle.
2. Implement Gradual Canary Rollouts
An OTA update can go catastrophically wrong if a bug slips through. Do not serve the new manifest to 100% of your devices immediately. Configure your internal database to support "Targeted Updates" based on client ID or device groups. We built our /api/manifest controller to inspect custom telemetry headers like x-device-id to roll updates out in phases:
- Phase 1: QA/Alpha devices (Internal testing)
- Phase 2: 5% of production users
- Phase 3: 100% of production users
3. Graceful Rollbacks
If an update is buggy, dropping a database record on your self-hosted server should automatically rollback the active manifest point to the previous stable release. Ensure your native app handles unexpected client-side crashes elegantly. If the app crashes more than 3 times on boot, use expo-updates' API to roll back local bundles dynamically:
Conclusion
Building your own Expo OTA updates infrastructure is no small feat. It requires handling custom cryptographic chains, setting up scalable static asset CDNs, and maintaining a robust database layer.
However, the payoffs are immense. For our enterprise clients, it completely bypassed compliance blockades, saved hundreds of thousands in third-party bandwidth charges, and brought total execution custody directly into the secure cloud boundaries where it belongs.
If you are operating at scale under high-compliance mandates, taking ownership of your OTA delivery stream isn’t just an optimization—it’s a necessary architectural evolution.