# iOS App Store Self-hosted iOS OTA distribution server. Node.js/Express + SQLite + Docker, deployed on unraid behind Nginx Proxy Manager. ## Live deployment - **URL**: https://appstore.treytartt.com - **Container**: `ios-appstore` on unraid (port `3080` internally, proxied via NPM at host `10.3.3.11:3080`) - **App code**: `/mnt/user/appdata/ios-appstore/` on unraid (Dockerfile, source, compose, `.env`) - **Data volume**: `/mnt/user/downloads/ios-appstore/` mounted as `/data` (SQLite DB, IPAs, icons) This split is intentional — app code in appdata, persistent data in downloads. Don't put data volumes in appdata or app source in downloads. The `.env` on the server holds `ADMIN_PASSWORD`, `API_TOKEN`, and `SESSION_SECRET`. Read it from `/mnt/user/appdata/ios-appstore/.env` when you need them. ## Deploy flow ```bash # 1. Sync local changes to unraid (excludes node_modules, data, .env) rsync -avz --exclude node_modules --exclude data --exclude .env \ /Users/m4mini/Desktop/code/ios-appstore/ \ unraid:/mnt/user/appdata/ios-appstore/ # 2. Rebuild and restart ssh unraid "cd /mnt/user/appdata/ios-appstore && docker compose up -d --build" # 3. Verify ssh unraid "docker logs ios-appstore --tail 20 && curl -s http://localhost:3080/api/health" ``` `docker-compose.yml` builds the image from local source — no registry. The data volume persists across rebuilds. ## Architecture - `src/server.js` — Express app, all routes, multer upload handling - `src/db.js` — SQLite schema (apps, builds, devices) - `src/ipa-parser.js` — Unzips IPA, extracts `Info.plist` and app icon - `src/manifest.js` — Generates the OTA manifest plist iOS fetches - `src/auth.js` — Session middleware (web UI) + token middleware (API) - `views/` — Login, app listing, upload pages (vanilla HTML) - `public/` — CSS + client-side JS Apps are keyed by bundle ID. Uploading the same bundle ID adds a new build to the existing app instead of duplicating it. ## Auth model Two parallel auth schemes: - **Session cookies** for browser users (login at `/login` with `ADMIN_PASSWORD`) - **`X-Api-Token` header** for CLI/automation (`API_TOKEN`) `requireAuth` accepts either. The manifest and IPA download endpoints (`/api/manifest/:id`, `/api/download/:id`) are intentionally **public** — iOS fetches them unauthenticated during the OTA install. ## OTA gotchas - **Development-signed IPAs cannot be installed OTA.** iOS only allows OTA installs of `ad-hoc` or `enterprise`-signed builds. If a user reports "integrity could not be verified", first check the export method in their `ExportOptions.plist`. - Ad-hoc requires a **distribution certificate** (not a development cert) and an **ad-hoc provisioning profile** with the target device UDIDs registered. - HTTPS is mandatory and must use a trusted CA. Self-signed certs don't work on iOS 12+. NPM's Let's Encrypt cert handles this. - The manifest's `bundle-identifier` must exactly match the IPA's bundle ID. ## Testing changes For backend changes, after deploying: ```bash # Health curl -s https://appstore.treytartt.com/api/health # List apps (needs token) curl -s -H "X-Api-Token: $TOKEN" https://appstore.treytartt.com/api/apps # Upload an IPA curl -X POST https://appstore.treytartt.com/api/upload \ -H "X-Api-Token: $TOKEN" \ -F "ipa=@/path/to/App.ipa" \ -F "notes=test" ``` Get `$TOKEN` from `/mnt/user/appdata/ios-appstore/.env` on unraid. For frontend changes, just rsync + restart and refresh the browser.