Rewrites the builder console to browse local Xcode projects instead of accepting source uploads or git URLs. Replaces the devices page with a profiles page that manages ad-hoc provisioning profiles and lists registered bundle IDs per team. Adds multi-account support: ASC API keys are now stored in an asc_keys table keyed by team_id (team_name, key_id, issuer_id, p8_filename). At build time, the worker reads DEVELOPMENT_TEAM from the Xcode project and auto-picks the matching key for fastlane sigh + JWT signing. Legacy single-key settings auto-migrate on first boot. Fixes storefront IPA parser to handle binary plists produced by Xcode. Drops the enrollment bridge, device management routes, and direct ASC API client -- fastlane sigh handles profile lifecycle now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS App Store
Self-hosted iOS OTA app distribution server. Build with your dev/distribution certs, host the IPAs, install on registered devices over the air — no Xcode tether, no TestFlight.
Features
- Web UI for browsing and installing apps (mobile-friendly, dark theme)
- Drag-and-drop IPA upload with automatic metadata + icon extraction
- CLI/API uploads with token auth
- Automatic manifest plist generation for
itms-services://installs - Multiple builds per app, version history, release notes
- Password-protected web UI, token-protected API
Requirements
- iOS device UDID registered in the provisioning profile embedded in the IPA
- IPA exported with
method: ad-hoc(development-signed IPAs cannot be installed OTA) - Server reachable over HTTPS with a trusted certificate
Stack
Node.js + Express + SQLite + Docker. Sharp for icon resizing. No client framework — vanilla HTML/CSS/JS.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/ |
session | App listing |
GET |
/upload |
session | Upload page |
POST |
/api/upload |
token or session | Upload IPA |
GET |
/api/apps |
token or session | List apps + latest build |
GET |
/api/apps/:id |
token or session | App detail with all builds |
DELETE |
/api/apps/:id |
token or session | Delete app + all builds |
DELETE |
/api/builds/:id |
token or session | Delete a single build |
GET |
/api/manifest/:buildId |
public | Plist manifest for OTA install |
GET |
/api/download/:buildId |
public | Download IPA file |
GET |
/api/health |
public | Health check |
The manifest and download endpoints are intentionally public — iOS fetches them unauthenticated during install.
CLI Upload
curl -X POST https://appstore.example.com/api/upload \
-H "X-Api-Token: $API_TOKEN" \
-F "ipa=@path/to/YourApp.ipa" \
-F "notes=Build notes"
Response includes the install URL (itms-services://...) ready to open in Safari on the device.
Configuration
Copy .env.example to .env and set:
| Variable | Description |
|---|---|
ADMIN_PASSWORD |
Web UI login password |
API_TOKEN |
Token for CLI/automation uploads |
SESSION_SECRET |
Express session secret |
BASE_URL |
Public HTTPS URL of the server |
PORT |
Listen port (default 3000) |
Deployment
Runs as a single Docker container. The /data volume holds the SQLite DB, uploaded IPAs, and extracted icons.
docker compose up -d --build
Put a reverse proxy with HTTPS in front of it. iOS requires a trusted SSL certificate for OTA installs to work.
Project Layout
src/
server.js Express app, routes, middleware
db.js SQLite schema
ipa-parser.js Extracts Info.plist + icon from IPA
manifest.js Generates OTA manifest plist
auth.js Session + token auth middleware
public/ Static assets (CSS, JS)
views/ HTML pages
Description
Languages
JavaScript
68.5%
CSS
16.7%
HTML
13.2%
Shell
0.8%
Ruby
0.6%
Other
0.2%