Skip to content

Repository files navigation

OTA Hot Updater Server

English | Tiếng Việt

If you're using hot-updater in a React Native app, you need an OTA server. This is the simplest one that actually works in production.

A minimal Node.js server that plugs directly into the hot-updater CLI — no vendor lock-in, runs anywhere Docker runs.

  • Flexible storage: local disk or S3/R2/MinIO — swap with one env variable
  • Production-ready security: API key auth, dashboard token, per-IP rate limiting — all built-in
  • Web dashboard: manage bundles, enable/disable, force update, archive for audit trail

How it works

React Native App  ──check──▶  /hot-updater/check   (public)
                  ◀─bundle─   /bundles/*            (local storage)

hot-updater CLI   ──deploy──▶ /hot-updater/bundles  (Bearer API key)
                  ──patch───▶ /hot-updater/bundles/:id

Browser           ──open────▶ /dashboard?token=...  (dashboard token)

Storage and database are selected at startup via STORAGE_MODE and DATABASE_MODE:

Mode Database Storage Best for
Local (default) SQLite on disk Disk + HTTP serve Dev, UAT, small self-hosted
S3 AWS S3 metadata AWS S3 / R2 / MinIO Scalable production
Technology stack
Component Technology
Runtime Node.js 20+ / TypeScript
Web Framework Hono + @hono/node-server
OTA Engine @hot-updater/server
Database SQLite (better-sqlite3 + kysely) / AWS S3 (@hot-updater/aws)
Storage Local Disk / AWS S3 (@hot-updater/aws)
Deployment Docker, Docker Compose, PM2

Why this solution? (Comparison)

If you are evaluating OTA solutions for a React Native application—especially CLI / Bare apps or established legacy codebases—here is how this setup compares:

Criteria ota-hot-updater-server expo-updates (EAS) Microsoft CodePush
React Native CLI (Bare) Native 100%, zero Expo footprint ⚠️ Requires install-expo-modules (~50+ packages) ✅ Native
Legacy Codebases (RN 0.70+) Zero migration risk (add ~3 native lines) ❌ High risk of AppDelegate/MainApplication conflicts ⚠️ Deprecated / App Center retired
Infrastructure & Control 🔒 Self-hosted: SQLite / AWS S3 / R2 / MinIO ☁️ Vendor lock-in (EAS Cloud) ☁️ Vendor lock-in (Azure)
Operational Cost 💰 $0 – $5/month (runs on existing VPS) 💸 Free tier limits, $99+/month for production 💸 Azure consumption billing
Web Dashboard 📊 Included out of the box with audit trail 🌐 EAS Web portal / CLI 🖥️ App Center Portal (retiring)
Crash Loop Defense 🛡️ Built-in (getCrashHistory) ⚠️ Must implement custom native checks ⚠️ Must implement custom rollback

Best Fit For:

  • React Native CLI (Bare) projects that do not want heavy Expo dependencies.
  • Legacy codebases with custom native modules, Firebase, or patched dependencies where adding Expo modules could break the build.
  • Teams needing data sovereignty (hosting OTA bundles within company-owned S3 buckets or private servers).
  • Projects migrating away from CodePush / App Center.

Quick Start

Local (no Docker)

pnpm install
cp .env.example .env        # set HOT_UPDATER_API_KEY and DASHBOARD_TOKEN
npm run dev

Docker Compose

cp .env.example .env

# Generate secure tokens
sed -i '' "s/YOUR_HOT_UPDATER_API_KEY_HERE/$(openssl rand -hex 32)/g" .env
sed -i '' "s/YOUR_DASHBOARD_TOKEN_HERE/$(openssl rand -hex 32)/g" .env

docker compose up -d --build
docker compose logs -f

Once running:

╔══════════════════════════════════════════════════╗
║        OTA Hot Updater Server — READY            ║
╠══════════════════════════════════════════════════╣
║  URL        http://localhost:3000                ║
║  Mode       LOCAL                                ║
║  Dashboard  http://localhost:3000/dashboard?...  ║
║  Health     http://localhost:3000/health         ║
║  API        http://localhost:3000/hot-updater    ║
╚══════════════════════════════════════════════════╝

Configuration (.env)

Minimum required for production:

NODE_ENV=production
HOT_UPDATER_API_KEY=<openssl rand -hex 32>
DASHBOARD_TOKEN=<openssl rand -hex 32>
PUBLIC_URL=https://ota.your-domain.com

Full reference:

Variable Description Default
PORT Listening port 3000
NODE_ENV development / production development
HOT_UPDATER_API_KEY Bearer token for deploy & mutation APIs required
DASHBOARD_TOKEN Web dashboard access token required
PUBLIC_URL Public server URL (used in bundle download URLs & CORS) http://localhost:PORT
DATABASE_MODE sqlite or s3 sqlite
STORAGE_MODE local or s3 local
MAX_UPLOAD_SIZE Max upload size in bytes (local mode) 104857600 (100 MB)
RATE_LIMIT_MAX Max requests per IP per window 100
RATE_LIMIT_WINDOW_MS Rate limit window in ms 900000 (15 min)
S3_BUCKET_NAME S3 bucket for bundles (STORAGE_MODE=s3)
S3_METADATA_BUCKET S3 bucket for metadata (DATABASE_MODE=s3) S3_BUCKET_NAME
S3_REGION AWS region ap-southeast-1
S3_ACCESS_KEY_ID AWS access key
S3_SECRET_ACCESS_KEY AWS secret key
S3_ENDPOINT Custom endpoint for MinIO / Cloudflare R2

Deploying the Server

Remote host via SSH (deploy.sh)

Builds a linux/amd64 Docker image locally, ships it to your server over SSH, and restarts the container:

# Syntax: ./deploy.sh <env_file> [server_ssh_host] [remote_dir]
./deploy.sh .env.prod my-vps /srv/ota-hot-updater-server

The script validates that HOT_UPDATER_API_KEY, DASHBOARD_TOKEN, and (if using S3) S3 credentials are set before building — it won't deploy with placeholder values.

Nginx reverse proxy (recommended)

server {
    server_name ota.your-domain.com;
    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
PM2 (without Docker)
pm2 start ecosystem.config.cjs
pm2 logs ota-hot-updater-server
pm2 save && pm2 startup

React Native Integration

Full step-by-step guide (for developers and AI agents):
👉 docs/CLIENT_INTEGRATION.md · Tiếng Việt

Reference code and patches are in examples/react-native-integration/.


Deploying OTA Bundles

# Deploy to UAT (iOS)
npx hot-updater deploy --platform ios --channel uat -m "Fix login issue"

# Deploy to Production (Android)
npx hot-updater deploy --platform android --channel production -m "Hotfix v1.2.1"

# Rollback
npx hot-updater rollback --platform ios --channel uat

Use scripts/deploy-ota.sh for a safety-guarded version that requires explicit confirmation before pushing to production.


Web Dashboard

Web Dashboard

https://ota.your-domain.com/dashboard?token=<DASHBOARD_TOKEN>
  • Overview & Filtering: View all bundles grouped by channel (production, uat, etc.) and platform (ios, android)
  • Bundle Control: Toggle Enable/Disable, Force Update, Archive (with audit trail), and Permanently Delete
  • Safety Safeguards: Type-to-confirm protection for production deletes
  • Batch Operations: Multi-select actions with live progress indicator
  • Inspect Metadata: View git commit hash, target app version, bundle file size, and direct artifact downloads

API Reference

Method Endpoint Auth Description
GET /health Public Status and uptime
GET /health/detail Bearer Full config diagnostic
GET /dashboard Token query Web management UI
GET /hot-updater/check Public Client update check
POST /hot-updater/bundles Bearer Deploy new bundle
PATCH /hot-updater/bundles/:id Bearer Update bundle metadata
DELETE /hot-updater/bundles/:id Bearer Delete bundle
GET /bundles/* Public Bundle file download (local mode)
PUT /bundles/:key Bearer Bundle file upload (local mode)

Production Checklist

  • HOT_UPDATER_API_KEY generated with openssl rand -hex 32
  • DASHBOARD_TOKEN generated with openssl rand -hex 32
  • NODE_ENV=production and PUBLIC_URL set correctly
  • .env is in .gitignore and not committed
  • /health returns { "status": "ok" }
  • Bundle deployed to uat, update confirmed on device
  • Rollback tested on uat
  • Backup scheduled for data/ (SQLite) or bucket versioning enabled (S3)

License

MIT

About

Self-hostable OTA update solution for React Native apps, alternative to CodePush.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages