May 2025 · 10 min read
Self-Hosted OTA Updates for React Native: Complete Guide
Over-the-air updates let you ship JavaScript changes to React Native apps without going through the App Store or Play Store review process. Self-hosting your OTA server gives you full control over the pipeline, predictable costs, and no dependency on a third-party service for a critical app function.
How OTA updates work in React Native
React Native apps consist of two layers: a native binary (compiled Objective-C/Swift or Java/Kotlin) and a JavaScript bundle. App Store rules require native binary changes to go through review. JavaScript bundle changes do not — they can be delivered at runtime.
An OTA update server works as follows:
- Developer builds a new JavaScript bundle and uploads it to the server
- On app launch, the app polls the server with its current version and platform
- If a compatible update exists, the server responds with a manifest URL
- The app downloads the new bundle and stores it locally
- On next launch (or immediately), the app runs the new bundle
The key constraint: you can only change JavaScript. Native module additions, new permissions, or changes to the native binary require a full app store submission.
Why self-host instead of using a managed service
Managed OTA services (Expo EAS Updates, previously CodePush) handle hosting, CDN, authentication, and the update protocol. The tradeoffs for self-hosting:
Cost. Managed services charge per-MAU or per-update. At 50,000 MAU, Expo EAS costs $299/month. Self-hosted infrastructure for the same load: $15–40/month. The gap widens with scale.
Data residency. Regulated industries (finance, healthcare, government) often cannot use US-hosted third-party services for app distribution infrastructure. Self-hosting lets you pick the region and cloud.
Control. You own the full pipeline. No service shutdowns, pricing changes, or API deprecations that break your release flow. CodePush teams found out the hard way when Microsoft shut down App Center in 2025.
Auditability. Security teams can review exactly what runs in your update pipeline. Open-source means no black boxes.
Architecture of a self-hosted OTA server
A production OTA server needs five components:
1. API server
Handles manifest requests from apps, authentication, release management, and CLI communication. Must be low-latency since it sits in the critical path of app startup. Typically a Node.js service (NestJS or Express).
2. Bundle storage
JavaScript bundles are 1–10 MB each. You need object storage with public read access and a CDN for fast global delivery. Cloudflare R2 is the most cost-effective option — no egress fees. AWS S3 + CloudFront or GCS also work.
3. Database
Stores release metadata: version, channel, platform, bundle URL, checksum, created_at. Also stores user accounts, organizations, and API keys. Postgres is the standard choice. Neon (serverless Postgres) works well for variable load with low base cost.
4. Cache layer
Manifest responses are identical for all users on the same app version and channel. Caching them in Redis reduces database load and cuts manifest response time from ~50ms to ~5ms. Upstash provides serverless Redis with a generous free tier.
5. Dashboard and CLI
A web UI for managing releases, channels, rollbacks, and teams. A CLI for publishing updates from local or CI environments.
Infrastructure requirements
| Component | Minimum | Recommended | Why |
|---|---|---|---|
| API server | 512 MB RAM, 0.5 vCPU | 1 GB RAM, 1 vCPU | Manifest parsing + auth |
| Database | Shared Postgres | Dedicated instance | Release query performance |
| Bundle storage | Any S3-compatible | Cloudflare R2 | No egress fees |
| Cache | Optional | Redis (Upstash free tier) | Manifest latency |
| CDN | Via R2/S3 | Cloudflare (automatic with R2) | Global bundle delivery |
The API server does not need to be powerful — manifest requests are lightweight. The main cost driver is bundle egress (how often users download new bundles). Cloudflare R2 eliminates egress fees, making it the clear choice for bundle storage.
Setting up Relay OTA
Relay OTA provides all five components as open-source software. Here is the recommended setup using Vercel (API), Neon (database), and Cloudflare R2 (storage).
Step 1: Clone and configure
git clone https://github.com/relayota/relay-ota
cd relay-ota
cp .env.example .envFill in your Neon connection string, R2 credentials, and a JWT secret.
Step 2: Run database migrations
cd apps/api
npx prisma migrate deployStep 3: Deploy the API
vercel deploy --prodOr deploy to Railway, Render, or any Node.js host. The API is a standard NestJS application.
Step 4: Configure your React Native app
// app.json
{
"expo": {
"runtimeVersion": "1.0.0",
"updates": {
"url": "https://your-relay-instance.com/api/manifest",
"enabled": true
}
}
}Step 5: Publish your first update
npx relay-ota publish \
--bundle ./dist \
--channel production \
--platform ios,android \
--runtime-version 1.0.0Your app will check for and apply this update on next cold launch.
CI/CD integration
Publish updates automatically on merge to main using GitHub Actions:
# .github/workflows/ota-publish.yml
name: Publish OTA Update
on:
push:
branches: [main]
paths: ['src/**', 'package.json']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx expo export --platform all
- run: |
npx relay-ota publish \
--bundle ./dist \
--channel production \
--platform ios,android
env:
RELAY_OTA_TOKEN: ${{ secrets.RELAY_OTA_TOKEN }}
RELAY_OTA_URL: ${{ secrets.RELAY_OTA_URL }}Cost breakdown at scale
| MAU | Relay OTA (self-hosted) | Expo EAS Updates | Monthly savings |
|---|---|---|---|
| 1,000 | $1–5 | $0 (free tier) | -$1–5 |
| 10,000 | $5–15 | $99 | $84–94 |
| 50,000 | $10–25 | $299 | $274–289 |
| 100,000 | $15–40 | $500+ | $460–485+ |
| 500,000 | $30–80 | $2,000+ | $1,920–1,970+ |
Self-hosting becomes economically positive above ~10,000 MAU, accounting for ~4 hours of setup time at typical developer rates. Above 50,000 MAU, the monthly savings justify ongoing maintenance easily.
Operational considerations
Before committing to self-hosting, plan for:
- Uptime: The manifest endpoint is in the critical path of app startup. Use a managed compute platform (Vercel, Railway) with automatic restarts and health checks rather than a bare VPS.
- Backups: Back up your Postgres database. Losing release metadata means losing rollback capability. Neon has point-in-time restore on paid plans.
- Runtime version discipline: OTA updates only apply to apps whose native binary matches the
runtimeVersion. Increment it whenever you ship a native binary update, or users on old binaries will get incompatible bundles. - Rollback plan: Always test rollback before your first production publish. Relay OTA supports one-click rollback via dashboard or CLI.
Get started
Relay OTA is open source under the MIT License. Full deployment guide, environment variable reference, and CI/CD examples are in the documentation.