Files
AppStore/CLAUDE.md
2026-04-11 11:42:19 -05:00

3.4 KiB

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

# 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:

# 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.