dkrypt is a self-hosted dashboard and API for headlessly decrypting App Store and TestFlight apps on a jailbroken iPhone or iPad.
- Decrypts current or pinned historical App Store releases by bundle ID.
- Browses and decrypts TestFlight builds.
- Queues jobs and keeps a persistent indexed IPA library with authenticated artifact downloads.
- Watches App Store releases on a schedule and dispatches authenticated artifact URLs to GitHub Actions.
- Protects scheduled watches from exhausting the GitHub API budget and shows their next 24 hours of runs.
- Runs a multi-user dashboard with OAuth, API keys, roles, billing, device health, logs, and backups.
- Gives admins per-user job, key, API-usage, and last-activity visibility.
- Docker Compose
- A jailbroken iPhone or iPad reachable over SSH
- autoinstall, installed on the device
- An Apple ID signed in to App Store on the device; TestFlight sign-in is needed only for beta builds
-
Copy
.env.exampleto.envand set at leastAPI_KEY,SESSION_SIGNING_SECRET,PUBLIC_BASE_URL, andADMIN_PASSWORD. -
Build the service:
docker compose build
-
Install or update autoinstall on the device:
make autoinstall-deploy
-
Ensure the device connection directory configured by
IPADECRYPT_ROOT_DIR(default/root/.ipadecrypt) contains aconfig.jsonwith the device host, port, SSH user, and key path. Additional device connection directories can be registered from Settings → Devices. -
Start dkrypt:
docker compose up -d
Open http://localhost:8080, or place your reverse proxy in front of it.
Self-hosting setup
Install Docker Compose, GNU make, and Git. Clone this repository, copy .env.example to .env, and set API_KEY, SESSION_SIGNING_SECRET, PUBLIC_BASE_URL, and ADMIN_PASSWORD to values for your deployment. Use an HTTPS PUBLIC_BASE_URL when enabling OAuth, Stripe, or external webhooks.
Keep the host's SSH private key outside the repository. Compose mounts $HOME/.ssh/id_ed25519_ipad into the container as /root/.ssh/id_ed25519; create that file or update the SSH-key volume in docker-compose.yml to match your key.
The device needs a rootless jailbreak with ElleKit, OpenSSH, AppSync Unified, appinst, and no passcode. Sign in to the App Store on the device; TestFlight builds also require TestFlight to be signed in. Verify SSH access from the host before continuing:
ssh -i "$HOME/.ssh/id_ed25519_ipad" mobile@<device-ip> 'uname -a'Install the autoinstall bridge from the repository root. Override the target and key when they differ from the defaults:
AUTOINSTALL_IPAD_TARGET=mobile@<device-ip> AUTOINSTALL_IPAD_KEY="$HOME/.ssh/id_ed25519_ipad" make autoinstall-deployThe release script builds the tweak, installs it, restarts the affected processes, checks the bridge heartbeat, and rolls back when verification fails.
Autoinstall handles on-device App Store and TestFlight installs. dkrypt still needs a small SSH connection file so it can reach the device and run the final decrypt. Create a temporary device-config.json with the connection details:
{
"device": {
"host": "<device-ip>",
"port": 22,
"user": "mobile",
"auth": {
"keyPath": "/root/.ssh/id_ed25519"
}
}
}Start the service, then copy the file into the persistent Compose volume:
docker compose up -d
docker compose cp device-config.json api:/root/.ipadecrypt/config.jsonThe default connection directory is /root/.ipadecrypt. Change it
with IPADECRYPT_ROOT_DIR if needed, and add a persistent Compose
volume mount for the replacement path. For additional devices, create a
separate directory under that persistent volume, copy a connection file there,
and register the directory in Settings → Devices:
docker compose exec api mkdir -p /root/.ipadecrypt/devices/device-b
docker compose cp device-b-config.json api:/root/.ipadecrypt/devices/device-b/config.jsonThe key path in each connection file is the path inside the container, not the host path. Do not commit the temporary JSON files or put private-key contents in them.
The API health response should report a reachable device, a reachable autoinstall bridge, and readiness: "ready":
docker compose exec api bun -e 'const response = await fetch("http://127.0.0.1:8080/v1/health", { headers: { authorization: "Bearer " + process.env.API_KEY } }); console.log(await response.text()); process.exit(response.ok ? 0 : 1)'If the device is unreachable, test SSH from inside the API container and confirm that the mounted key path and the config.json host, port, and user are correct. If the bridge is not ready, confirm that autoinstall is installed and that the App Store account is signed in without a device passcode.
Persistent state, connection configuration, and decrypted artifacts live in Docker volumes. To update a source-based installation, pull the new revision, rebuild, and recreate the service:
git pull --ff-only origin main
docker compose build
docker compose up -dPushes to main run the Moon check graph, generate the dashboard changelog from Git history, publish an immutable GHCR image, and deploy that exact digest on the homelab runner. The runner keeps only the runtime .env in /home/adrian/.local/share/dkrypt, pulls images, and never retains a source checkout. If its health check fails, it starts the previous image again.
The homelab runtime .env must include SESSION_SIGNING_SECRET alongside the other required authentication values.
Enable Managed Payments and accept its terms in Stripe Dashboard first. Then choose an eligible product tax code for dkrypt’s SaaS plans and set STRIPE_TAX_CODE before creating the monthly EUR catalog and hosted webhook from packages/dkrypt with a Stripe test or live secret:
bun run stripe:seed
bun run stripe:webhook
bun run stripe:verifyCopy the four price IDs printed by stripe:seed into the runtime environment, store the webhook secret printed for a newly created endpoint as STRIPE_WEBHOOK_SECRET, and set STRIPE_WEBHOOK_URL to the public /v1/stripe/webhook URL. stripe:verify checks the configured key mode, recurring price amounts, eligible product tax codes, Managed Payments Checkout compatibility, webhook URL, subscribed events, and signed endpoint reachability without printing any secret. Its Checkout probe is created and immediately expired without collecting payment details.
The listed API endpoints use Authorization: Bearer <API_KEY>. Dashboard downloads use the signed-in session, while API and scheduler downloads use Authorization: Bearer <API_KEY>. Artifact URLs remain valid until the underlying artifact is explicitly removed or evicted by storage quota.
Repositories receiving scheduler dispatches must define a DKRYPT_API_KEY Actions secret. The loader’s remote-deploy workflow enables authenticated artifact-source downloads with that secret; manual and other external IPA source downloads remain unauthenticated. For a deployment whose PUBLIC_BASE_URL is not https://ipa.dylib.dev, set the receiving repository variable DKRYPT_BASE_URL to the same host.
| Endpoint | Purpose |
|---|---|
GET /v1/decrypt?bundleId=<id> |
Queue or join a decrypt and return the IPA. |
GET /v1/decrypt?bundleId=<id>&externalVersionId=<id> |
Decrypt a pinned historical App Store release. |
POST /v1/decrypts |
Queue or reuse a decrypt by release selector (240, 234.2, or 240_109440). |
GET /v1/jobs/:id |
Read job status. |
GET /v1/artifacts |
List IPA artifacts. |
GET /v1/artifacts/:id/file |
Download an IPA artifact with an API key. |
GET /v1/health |
Read liveness and scheduler state. |
The POST /v1/decrypts selector accepts an optional leading v. A blank selector resolves the current App Store version and lets the signed-in device's autoinstall bridge perform the install. Historical App Store selectors use the available version history when an external version id is known; selectors containing an underscore target a TestFlight train and build. Completed artifacts survive job-history cleanup and remain available until the persistent artifact store reaches its configured 200 GiB limit; least-recently-used artifacts are evicted first.
| Path | Purpose |
|---|---|
packages/dkrypt/ |
Fastify API and Svelte dashboard |
packages/autoinstall/ |
Theos tweak installed on the device |
scripts/autoinstall-release |
Build, install, heartbeat-check, and roll back the tweak |
Moon manages the cross-language project graph and task targets. The Makefile remains a short convenience layer.
Moon is pinned in .prototools; install it with proto install before using the Moon commands.
| Moon project | Scope |
|---|---|
dkrypt |
Fastify API tests, dashboard checks, Compose build, and release tasks |
web |
Svelte dashboard type checks |
autoinstall |
Theos package and deployment tasks |
make check
make autoinstall-package
make autoinstall-deploy
make autoinstall-rollback PACKAGE=/absolute/path/to/package.deb
moon run dkrypt:check
moon run autoinstall:packageautoinstall-deploy uses the configured device target. Override the target or SSH key with AUTOINSTALL_IPAD_TARGET and AUTOINSTALL_IPAD_KEY.
cd packages/dkrypt
bun test
bun run typecheck
bun run typecheck:webFor scheduler watches, OAuth, Stripe, push notifications, and all optional integrations, use the documented environment variables in .env.example and configure them from the dashboard. Existing billing records from the previous provider are retained as legacy records and do not grant Stripe entitlements; review affected accounts before creating new Stripe subscriptions.