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
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 |
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 | install-expo-modules (~50+ packages) |
✅ Native |
| Legacy Codebases (RN 0.70+) | ✅ Zero migration risk (add ~3 native lines) | ❌ High risk of AppDelegate/MainApplication conflicts |
|
| 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) |
- 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.
pnpm install
cp .env.example .env # set HOT_UPDATER_API_KEY and DASHBOARD_TOKEN
npm run devcp .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 -fOnce 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 ║
╚══════════════════════════════════════════════════╝
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.comFull 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 | — |
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-serverThe 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.
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 startupFull 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/.
# 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 uatUse scripts/deploy-ota.sh for a safety-guarded version that requires explicit confirmation before pushing to production.
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
| 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) |
-
HOT_UPDATER_API_KEYgenerated withopenssl rand -hex 32 -
DASHBOARD_TOKENgenerated withopenssl rand -hex 32 -
NODE_ENV=productionandPUBLIC_URLset correctly -
.envis in.gitignoreand not committed -
/healthreturns{ "status": "ok" } - Bundle deployed to
uat, update confirmed on device - Rollback tested on
uat - Backup scheduled for
data/(SQLite) or bucket versioning enabled (S3)
