Version: 0.1.0-draft Date: February 25, 2026 Author: Skelpo GmbH
Perry Publish extends the Perry compiler with a complete build-sign-package-distribute pipeline, enabling developers to go from TypeScript source to published applications on all major platforms with a single command.
- Server-first by default, local build as opt-in for power users
- Zero cost for free-tier users — no servers idle, no storage persists
- Credentials never stored — everything in-memory, wiped after build
- Open source server — self-hostable, full transparency
- Progressive disclosure — free for one platform, paid for multi-platform
| Platform | Output Format | Signing Method | Distribution |
|---|---|---|---|
| macOS | .dmg / .app | Developer ID + Notarization | Direct / Homebrew |
| iOS | .ipa | App Store provisioning profile | App Store Connect |
| Android | .aab / .apk | Keystore signing | Google Play Console |
| Windows | .exe / .msix | Code signing certificate | Direct / Microsoft Store |
| Linux | .deb / .AppImage | GPG signing (optional) | Direct / apt repo |
$ perry publish --macos
Perry Publish v0.1.0
First time? Let's get you set up.
Authenticate with GitHub to get your free license.
→ Open https://github.com/login/device
→ Enter code: ABCD-1234
✓ Licensed to @username (free tier: 1 platform)
License saved to ~/.perry/config.toml
Now let's set up macOS signing.
Run: perry auth apple
$ perry auth apple
Perry needs an App Store Connect API key.
1. Go to https://appstoreconnect.apple.com/access/api
2. Click "Generate API Key" (requires Admin role)
3. Download the .p8 file
Enter Key ID: XXXXXXXXXX
Enter Issuer ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Select .p8 file: ~/Downloads/AuthKey_XXXXXXXXXX.p8
✓ Apple credentials saved to ~/.perry/credentials/apple.toml
✓ Credentials are encrypted at rest
Test connection? [Y/n] Y
✓ Connected to App Store Connect as "Ralph - Skelpo GmbH"
$ perry auth google
Perry needs a Google Play service account.
1. Go to https://play.google.com/console/developers
2. Settings → API access → Create service account
3. Download the JSON key file
Select JSON key file: ~/Downloads/service-account.json
✓ Google Play credentials saved to ~/.perry/credentials/google.toml
✓ Credentials are encrypted at rest
Test connection? [Y/n] Y
✓ Connected to Google Play Console as "Skelpo GmbH"
$ perry publish --ios
Reading perry.toml...
Packaging project (2.3 MB)...
⠋ Uploading to Perry Build Service...
✓ Uploaded (1.2s)
⠋ Compiling for iOS (aarch64-apple-ios)...
src/main.ts
src/ui.ts
src/api.ts
src/components/header.ts
✓ Compiled (4.2s)
⠋ Processing assets...
Generating App Icons (20 sizes from icon.png)
Generating Launch Screen
Compiling Asset Catalog
✓ Assets ready
⠋ Bundling MyApp.app...
Generating Info.plist
Embedding provisioning profile
Creating app bundle structure
✓ Bundled
⠋ Code signing...
Signing with "Apple Distribution: Skelpo GmbH (ABC123)"
Signing frameworks and embedded content
✓ Signed
⠋ Creating .ipa...
✓ Packaged
⠋ Uploading to App Store Connect...
Uploading MyApp-1.0.0.ipa (18.3 MB)
████████████████████████████████ 100%
Validating with App Store...
✓ Uploaded
🎉 MyApp 1.0.0 submitted to App Store Connect
Build: #1234
Status: Waiting for Review
Track at: https://appstoreconnect.apple.com/apps/1234567890
$ perry publish --ios --android --macos
⚠ Multi-platform publishing requires Perry Pro (€29/mo)
→ Upgrade at https://perry.dev/pro/@username
Already have a license key? Enter it: XXXX-XXXX-XXXX-XXXX
✓ Perry Pro activated
Starting parallel builds for 3 targets...
iOS (aarch64-apple-ios)
✓ Compiled (4.2s)
✓ Bundled & Signed
⠋ Uploading to App Store Connect...
Android (aarch64-linux-android)
✓ Compiled (3.8s)
✓ Bundled & Signed
⠋ Uploading to Google Play Console...
macOS (aarch64-apple-darwin)
✓ Compiled (3.1s)
✓ Bundled & Signed
⠋ Notarizing with Apple...
✓ All 3 targets published successfully
iOS: Waiting for Review
Android: In Review
macOS: MyApp-1.0.0.dmg ready
$ perry publish --macos --local
Building locally...
⚠ Local builds require toolchain dependencies.
Checking: codesign ✓ notarytool ✓ hdiutil ✓
⠋ Compiling for macOS (aarch64-apple-darwin)...
✓ Compiled (3.1s)
⠋ Bundling...
✓ Bundled
⠋ Signing...
✓ Signed
⠋ Notarizing...
✓ Notarized (43s)
⠋ Creating DMG...
✓ MyApp-1.0.0.dmg (24.1 MB)
Output: ./dist/MyApp-1.0.0.dmg
[app]
name = "My App" # Display name
version = "1.0.0" # Semver
description = "A cool app" # Short description
author = "Developer Name"
url = "https://myapp.dev"
entry = "src/main.ts" # Application entry point
[app.icons]
source = "assets/icon.png" # Minimum 1024x1024, Perry generates all sizes
# Optional per-platform overrides:
# macos = "assets/icon-macos.png"
# ios = "assets/icon-ios.png"
# android = "assets/icon-android.png"
[app.splash]
source = "assets/splash.png" # iOS/Android launch screen
background = "#FFFFFF" # Background color
# ─── Platform Targets ───────────────────────────────────────
[macos]
bundle_id = "com.developer.myapp"
category = "public.app-category.developer-tools" # LSApplicationCategoryType
minimum_os = "13.0"
sandbox = true
entitlements = [ # macOS sandbox entitlements
"com.apple.security.network.client",
"com.apple.security.files.user-selected.read-write"
]
# Distribution options
distribute = "direct" # "direct" (DMG) | "appstore" | "homebrew"
[ios]
bundle_id = "com.developer.myapp"
deployment_target = "16.0"
device_family = ["iphone", "ipad"] # or just ["iphone"]
orientations = ["portrait"] # portrait, landscape-left, landscape-right, upside-down
capabilities = [ # iOS capabilities / entitlements
"push-notifications",
"camera",
"photo-library"
]
app_category = "utilities" # App Store category
distribute = "appstore" # "appstore" | "testflight" | "adhoc"
[android]
package = "com.developer.myapp"
min_sdk = 26 # Android 8.0
target_sdk = 34 # Android 14
permissions = [
"INTERNET",
"CAMERA",
"READ_EXTERNAL_STORAGE"
]
features = [
{ name = "android.hardware.camera", required = false }
]
distribute = "playstore" # "playstore" | "direct" (APK)
track = "production" # "internal" | "alpha" | "beta" | "production"
[windows]
package_name = "com.developer.myapp"
minimum_os = "10.0.17763.0" # Windows 10 1809
capabilities = [
"internetClient",
"webcam"
]
distribute = "direct" # "direct" (EXE) | "msstore"
[linux]
package_name = "myapp"
categories = ["Development", "Utility"]
depends = [] # apt dependencies
distribute = "direct" # "direct" (AppImage) | "deb" | "rpm"
# ─── Build Service Options ──────────────────────────────────
[publish]
server = "https://build.perry.dev" # Default Perry Build Service
# server = "https://build.mycompany.com" # Self-hosted
# server = "local" # Force local builds
timeout = 600 # Build timeout in secondsThe absolute minimum to publish an iOS app:
[app]
name = "My App"
version = "1.0.0"
entry = "src/main.ts"
[app.icons]
source = "icon.png"
[ios]
bundle_id = "com.me.myapp"Everything else has sensible defaults. Perry infers what it can and fills in the rest.
~/.perry/
├── config.toml # License key, GitHub identity, preferences
├── credentials/
│ ├── apple.toml # App Store Connect API key (encrypted)
│ ├── google.toml # Play Store service account (encrypted)
│ ├── windows.toml # Windows code signing cert (encrypted)
│ └── keystore.jks # Android signing keystore (encrypted)
└── cache/
└── ... (build cache, optional)
Credential files are encrypted at rest using a key derived from the machine's hardware identity. They can only be decrypted on the same machine. This means credentials are usable but not extractable.
The Perry Build Server is a standalone open source Rust application that receives build requests over WebSocket, compiles and packages applications, and returns artifacts or publishes directly to app stores.
┌─────────────────────────┐
│ Cloudflare Tunnel │
│ build.perry.dev │
└────────┬────────────────┘
│
┌────────▼────────────────┐
│ Mac Mini M4 │
│ │
│ perry-build-server │
│ ├── Rust/Axum │
│ ├── Job Queue │
│ └── Build Workers │
│ │
│ Toolchains: │
│ ├── Perry compiler │
│ ├── Apple SDK │
│ ├── Android SDK │
│ ├── Windows xtools │
│ └── Linux targets │
│ │
│ NO persistent storage │
│ NO database │
│ NO user data at rest │
└─────────────────────────┘
perry-build-server/
├── Cargo.toml
├── src/
│ ├── main.rs # Entry point, Axum server setup
│ ├── config.rs # Server configuration
│ ├── api/
│ │ ├── mod.rs
│ │ ├── routes.rs # HTTP endpoints
│ │ └── ws.rs # WebSocket handler
│ ├── auth/
│ │ ├── mod.rs
│ │ ├── license.rs # License key validation
│ │ └── github.rs # GitHub device flow
│ ├── queue/
│ │ ├── mod.rs
│ │ ├── job.rs # Job definition
│ │ └── worker.rs # Worker pool
│ ├── build/
│ │ ├── mod.rs
│ │ ├── pipeline.rs # Build orchestration
│ │ ├── compiler.rs # Runs perry build
│ │ ├── assets.rs # Icon/splash generation
│ │ └── cleanup.rs # Secure tmpdir wipe
│ ├── package/
│ │ ├── mod.rs
│ │ ├── macos.rs # .app bundle + .dmg creation
│ │ ├── ios.rs # .app bundle + .ipa creation
│ │ ├── android.rs # .apk / .aab creation
│ │ ├── windows.rs # .exe / .msix creation
│ │ └── linux.rs # .deb / .AppImage creation
│ ├── signing/
│ │ ├── mod.rs
│ │ ├── apple.rs # codesign + notarytool
│ │ ├── android.rs # apksigner / jarsigner
│ │ ├── windows.rs # osslsigncode
│ │ └── gpg.rs # Linux package signing
│ ├── publish/
│ │ ├── mod.rs
│ │ ├── appstore.rs # App Store Connect API
│ │ ├── playstore.rs # Google Play Developer API
│ │ ├── msstore.rs # Microsoft Store submission API
│ │ └── homebrew.rs # Homebrew formula generation
│ └── ws/
│ ├── mod.rs
│ └── messages.rs # WebSocket message types
├── tests/
│ ├── integration/
│ │ ├── build_macos.rs
│ │ ├── build_ios.rs
│ │ ├── build_android.rs
│ │ └── signing.rs
│ └── fixtures/
│ └── sample_project/
└── README.md
Phase 1: UPLOAD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Client:
1. Reads perry.toml
2. Creates tarball of project directory
- Respects .perryignore (like .gitignore)
- Excludes: node_modules, .git, dist, build
3. Reads credentials from ~/.perry/credentials/
4. Opens WebSocket to build.perry.dev
5. Sends initial handshake:
→ HTTP POST /api/v1/build (upgrades to WebSocket)
Request body (multipart):
┌──────────────────────────────────────────┐
│ project.tar.gz (source code) │
│ manifest.json (see §4.5) │
│ credentials.json (see §4.6) │
└──────────────────────────────────────────┘
Phase 2: BUILD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server:
1. Validates license key
2. Creates isolated tmpdir: /tmp/perry-build-{uuid}/
3. Extracts tarball
4. Streams progress via WebSocket
┌─ For each target platform: ─────────────────────────┐
│ │
│ a. perry build --target {target_triple} │
│ → Compiles TypeScript to native binary │
│ → Streams compiler output to client │
│ │
│ b. Asset processing │
│ → Resize icon.png to all required sizes │
│ → Generate asset catalogs (iOS: Assets.car) │
│ → Generate adaptive icons (Android) │
│ → Create launch screens │
│ │
│ c. Bundle creation (see §5 per platform) │
│ → Create platform-specific app structure │
│ → Generate Info.plist / AndroidManifest.xml │
│ → Embed resources and assets │
│ │
│ d. Code signing (credentials held in memory) │
│ → Sign binary and bundle │
│ → Platform-specific signing (see §6) │
│ │
│ e. Package final artifact │
│ → .ipa / .dmg / .aab / .exe / .AppImage │
│ │
└──────────────────────────────────────────────────────┘
Phase 3: DISTRIBUTE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server (based on distribute setting in perry.toml):
IF distribute = "appstore" / "playstore" / "msstore":
→ Upload artifact to store API using user's credentials
→ Stream upload progress
→ Return store confirmation / review status URL
IF distribute = "direct" / "testflight" / "adhoc":
→ Write artifact to temp location
→ Return short-lived download URL (10 min TTL)
→ Client downloads artifact
→ Server deletes artifact
Phase 4: CLEANUP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server (IMMEDIATELY after build completes or fails):
1. Wipe tmpdir: rm -rf /tmp/perry-build-{uuid}/
2. Zero credentials from memory
3. Delete any temporary artifacts after download or timeout
4. No logs retained containing source code or credentials
Sent by the CLI with each build request:
{
"version": "1",
"license_key": "XXXX-XXXX-XXXX-XXXX",
"github_username": "ralph",
"targets": ["aarch64-apple-ios"],
"perry_version": "0.5.0",
"app": {
"name": "My App",
"version": "1.0.0",
"bundle_id": "com.developer.myapp",
"entry": "src/main.ts"
},
"platform_config": {
"ios": {
"deployment_target": "16.0",
"device_family": ["iphone", "ipad"],
"orientations": ["portrait"],
"capabilities": ["push-notifications"],
"distribute": "appstore"
}
},
"options": {
"upload": true,
"timeout": 600
}
}Sent encrypted in transit (TLS), held only in-memory on server:
{
"apple": {
"key_id": "XXXXXXXXXX",
"issuer_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"p8_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
"team_id": "ABC123DEF",
"signing_identity": "Apple Distribution: Skelpo GmbH (ABC123DEF)"
},
"android": {
"keystore": "<base64-encoded keystore>",
"keystore_password": "...",
"key_alias": "upload",
"key_password": "...",
"service_account": { /* Google Play service account JSON */ }
},
"windows": {
"certificate": "<base64-encoded PFX>",
"password": "..."
}
}MyApp.app/
├── Contents/
│ ├── Info.plist # Generated from perry.toml
│ ├── MacOS/
│ │ └── MyApp # Native binary (from perry build)
│ ├── Resources/
│ │ ├── AppIcon.icns # Generated from icon.png
│ │ ├── Assets.car # Compiled asset catalog (if needed)
│ │ └── en.lproj/
│ │ └── InfoPlist.strings
│ ├── Frameworks/ # Embedded frameworks (if any)
│ └── _CodeSignature/
│ └── CodeResources # Generated by codesign
Info.plist generation — Perry generates this entirely from perry.toml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key> <string>MyApp</string>
<key>CFBundleIdentifier</key> <string>com.developer.myapp</string>
<key>CFBundleName</key> <string>My App</string>
<key>CFBundleVersion</key> <string>1</string>
<key>CFBundleShortVersionString</key> <string>1.0.0</string>
<key>CFBundleIconFile</key> <string>AppIcon</string>
<key>LSMinimumSystemVersion</key> <string>13.0</string>
<key>LSApplicationCategoryType</key> <string>public.app-category.developer-tools</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>NSHighResolutionCapable</key> <true/>
<!-- Sandbox entitlements reference -->
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
</dict>
</plist>DMG creation (no Xcode required):
# Create temporary DMG directory
mkdir -p dmg_contents
cp -R MyApp.app dmg_contents/
ln -s /Applications dmg_contents/Applications
# Create DMG
hdiutil create -volname "My App" \
-srcfolder dmg_contents \
-ov -format UDZO \
MyApp-1.0.0.dmgPayload/
└── MyApp.app/
├── Info.plist # Generated, includes UIDevice requirements
├── MyApp # Native binary (ARM64)
├── Assets.car # Compiled asset catalog (icons, images)
├── LaunchScreen.storyboardc # Compiled launch screen (or generated)
├── embedded.mobileprovision # Provisioning profile
├── Frameworks/ # Embedded frameworks
└── _CodeSignature/
└── CodeResources
IPA creation (no Xcode required):
# The .ipa is literally a zip of the Payload directory
mkdir -p Payload
cp -R MyApp.app Payload/
zip -r MyApp.ipa Payload/Key challenge: Provisioning profiles. The user needs to have created a provisioning profile in their Apple Developer account that matches the bundle ID. Perry can guide them through this or automate it via the App Store Connect API.
Asset catalog compilation (requires actool from Xcode CLI tools, NOT full Xcode):
# Only Xcode Command Line Tools needed, not full Xcode
xcrun actool Assets.xcassets \
--compile output/ \
--platform iphoneos \
--minimum-deployment-target 16.0Note: Investigate alternatives to
actoolfor fully Xcode-free builds. The asset catalog format is documented and could be generated in pure Rust. This is a stretch goal for v1.0.
APK structure:
├── AndroidManifest.xml # Generated from perry.toml (binary XML)
├── classes.dex # Empty or minimal (native app)
├── lib/
│ └── arm64-v8a/
│ └── libmyapp.so # Native binary (from perry build)
├── res/
│ ├── mipmap-xxxhdpi/
│ │ └── ic_launcher.png # Generated from icon.png
│ ├── mipmap-xxhdpi/
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi/
│ │ └── ic_launcher.png
│ ├── mipmap-hdpi/
│ │ └── ic_launcher.png
│ └── mipmap-mdpi/
│ └── ic_launcher.png
├── resources.arsc # Compiled resources
└── META-INF/
└── CERT.SF / CERT.RSA # Signing info
AAB (Android App Bundle) — preferred for Play Store, similar structure but uses Google's bundletool format.
No Android Studio required. Build process:
- Perry compiles to
libmyapp.so(aarch64-linux-android target) - Generate
AndroidManifest.xmlfrom perry.toml - Resize icons to all density buckets
- Use
aapt2to compile resources (standalone binary, no IDE) - Package into APK/AAB
- Sign with
apksigner(standalone, from Android SDK build-tools)
Android SDK build-tools can be installed standalone (~200MB) without Android Studio (~2GB+).
MyApp/
├── MyApp.exe # Native binary (from perry build)
├── app.manifest # Windows application manifest
├── resources.rc # Resource file (icon, version info)
└── MyApp.ico # Generated from icon.png
MSIX package (for Microsoft Store):
MyApp.msix
├── AppxManifest.xml # Generated from perry.toml
├── Assets/
│ ├── Square150x150Logo.png
│ ├── Square44x44Logo.png
│ ├── StoreLogo.png
│ └── Wide310x150Logo.png
├── MyApp.exe
└── AppxBlockMap.xml
No Visual Studio required. Code signing on Linux/macOS:
# osslsigncode works on all platforms
osslsigncode sign \
-pkcs12 cert.pfx \
-pass "password" \
-n "My App" \
-t http://timestamp.digicert.com \
-in MyApp.exe \
-out MyApp-signed.exeAppImage structure:
├── AppRun # Entry point script
├── myapp.desktop # Desktop entry file
├── myapp.png # App icon
└── usr/
├── bin/
│ └── myapp # Native binary
└── share/
└── applications/
└── myapp.desktop
DEB structure:
├── DEBIAN/
│ ├── control # Package metadata
│ ├── postinst # Post-install script (optional)
│ └── prerm # Pre-remove script (optional)
└── usr/
├── bin/
│ └── myapp
└── share/
├── applications/
│ └── myapp.desktop
└── icons/
└── hicolor/
└── 256x256/
└── apps/
└── myapp.png
Step 1: Code sign the binary
codesign --force --options runtime \
--sign "Developer ID Application: Name (TEAM_ID)" \
--entitlements entitlements.plist \
MyApp.app
Step 2: Notarize
xcrun notarytool submit MyApp.dmg \
--key /path/to/key.p8 \
--key-id KEY_ID \
--issuer ISSUER_ID \
--wait
Step 3: Staple (after notarization approved)
xcrun stapler staple MyApp.dmg
Without Xcode: codesign ships with macOS. notarytool and stapler ship with Xcode Command Line Tools (small install, not full Xcode). Alternatively, notarization can be done entirely via the App Store Connect REST API — no local tools needed on the build server beyond codesign.
Step 1: Sign the .app
codesign --force \
--sign "Apple Distribution: Name (TEAM_ID)" \
--entitlements entitlements.plist \
Payload/MyApp.app
Step 2: Package .ipa
zip -r MyApp.ipa Payload/
Step 3: Upload via App Store Connect API
# REST API - no local tools needed
POST https://api.appstoreconnect.apple.com/v1/builds
Step 1: Build unsigned APK/AAB
(perry handles this)
Step 2: Zipalign
zipalign -v 4 unsigned.apk aligned.apk
Step 3: Sign
apksigner sign \
--ks keystore.jks \
--ks-pass pass:PASSWORD \
--ks-key-alias upload \
aligned.apk
Step 4: Upload via Play Developer API
# REST API
POST https://androidpublisher.googleapis.com/...
Step 1: Sign EXE
osslsigncode sign \
-pkcs12 certificate.pfx \
-pass PASSWORD \
-n "My App" \
-t http://timestamp.digicert.com \
-in MyApp.exe \
-out MyApp-signed.exe
Step 2: (Optional) Create MSIX and sign
# makemsix or MSIX Packaging Tool
Client Server
│ │
├── POST /api/v1/build ────────────→│ Upload tarball + manifest + credentials
│ │ Validate license
│ │ Create job
│←── 101 Switching Protocols ───────│ Upgrade to WebSocket
│ │
│←── { type: "job_created" } ──────│
│←── { type: "stage" } ──────│
│←── { type: "log" } ──────│ ... repeated
│←── { type: "progress" } ──────│ ... repeated
│←── { type: "stage" } ──────│
│←── { type: "complete" } ──────│
│ │
│── close ──────────────────────────│
Server → Client messages:
// Build job accepted
{
type: "job_created",
job_id: "uuid",
position: 0, // Queue position (0 = building now)
estimated_seconds: 30
}
// Queue update (if waiting)
{
type: "queue_update",
position: 2,
estimated_seconds: 60
}
// Build stage transition
{
type: "stage",
name: "compiling" | "assets" | "bundling" | "signing" |
"notarizing" | "packaging" | "uploading" | "complete" | "failed",
target: "aarch64-apple-ios" // Which platform (for parallel builds)
}
// Log line
{
type: "log",
message: "compiling src/main.ts",
target: "aarch64-apple-ios",
timestamp: "2026-02-25T14:30:00Z"
}
// Progress update
{
type: "progress",
percent: 45,
target: "aarch64-apple-ios"
}
// Build complete — artifact available for download
{
type: "artifact_ready",
target: "aarch64-apple-ios",
artifact_url: "https://build.perry.dev/dl/tmp-abc123",
artifact_name: "MyApp-1.0.0.ipa",
artifact_size: 18_300_000,
expires_at: "2026-02-25T14:40:00Z", // 10 minute TTL
checksums: {
sha256: "abc123..."
}
}
// Build complete — published to store
{
type: "published",
target: "aarch64-apple-ios",
store: "appstore",
status: "waiting_for_review",
url: "https://appstoreconnect.apple.com/apps/1234567890"
}
// Error
{
type: "error",
code: "SIGNING_FAILED",
message: "Your App Store Connect API key has expired.",
hint: "Generate a new one at:\nhttps://appstoreconnect.apple.com/access/api\n\nThen run: perry auth apple",
target: "aarch64-apple-ios",
fatal: true // true = build stopped, false = warning
}
// Final summary
{
type: "complete",
success: true,
duration_seconds: 47,
targets: {
"aarch64-apple-ios": {
status: "published",
store_url: "..."
},
"aarch64-apple-darwin": {
status: "artifact_ready",
download_url: "..."
}
}
}Client → Server messages:
// Cancel build
{
type: "cancel",
job_id: "uuid"
}
// Keepalive (every 30s)
{
type: "ping"
}| Code | Meaning | User Hint |
|---|---|---|
LICENSE_INVALID |
License key not recognized | Check key at perry.dev/account |
LICENSE_TIER |
Feature requires Pro | Upgrade at perry.dev/pro |
UPLOAD_TOO_LARGE |
Project tarball exceeds limit | Check .perryignore, max 100MB |
COMPILE_FAILED |
Perry compilation error | Fix source code errors shown in log |
SIGNING_FAILED |
Code signing failed | Check credentials with perry auth check |
SIGNING_EXPIRED |
Certificate/key expired | Renew at provider, run perry auth {platform} |
PROFILE_MISMATCH |
Bundle ID vs provisioning profile | Ensure IDs match in Apple Developer portal |
NOTARIZE_FAILED |
Apple rejected notarization | Check Apple's rejection reason in logs |
NOTARIZE_TIMEOUT |
Apple took too long | Retry with perry publish --ios |
UPLOAD_FAILED |
Store upload failed | Check network, retry |
STORE_REJECTED |
Store rejected binary | Check store-specific error in logs |
QUEUE_FULL |
Build queue is full | Retry in a few minutes |
TIMEOUT |
Build exceeded timeout | Increase timeout in perry.toml or simplify build |
SERVER_ERROR |
Internal server error | Retry, if persistent report at github.com/perrydev |
CREDENTIAL_MISSING |
No credentials for target | Run perry auth {platform} |
POST /api/v1/build
Content-Type: multipart/form-data
Authorization: Bearer {license_key}
Parts:
project - application/gzip (tarball)
manifest - application/json
credentials - application/json (encrypted in transit via TLS)
Response: 101 Upgrade to WebSocket
Error responses:
401 - Invalid license key
402 - Tier upgrade required
413 - Project too large
429 - Rate limited
503 - Queue full
GET /api/v1/dl/{token}
Response: application/octet-stream
The token is single-use and expires after 10 minutes.
Error responses:
404 - Token expired or invalid
410 - Artifact already downloaded
POST /api/v1/license/register
Body: { "github_username": "ralph", "github_token": "..." }
Response: { "license_key": "FREE-XXXX-XXXX-XXXX", "tier": "free" }
POST /api/v1/license/verify
Body: { "license_key": "XXXX-XXXX-XXXX-XXXX" }
Response: { "valid": true, "tier": "free", "platforms": 1 }
GET /api/v1/status
Response: {
"status": "ok",
"queue_length": 2,
"perry_version": "0.5.0",
"supported_targets": [
"aarch64-apple-darwin",
"aarch64-apple-ios",
"aarch64-linux-android",
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc"
]
}
- Zero trust in persistence. No source code, credentials, or artifacts are written to permanent storage. Everything lives in tmpdir and memory.
- In-memory credentials. User credentials are deserialized into memory, used during the build, then explicitly zeroed (using
zeroizecrate in Rust). - Process isolation. Each build runs in its own tmpdir with restricted filesystem access.
- TLS everywhere. All communication between CLI and build server is over TLS.
- Open source verification. The entire server is open source. Users can audit exactly what happens with their credentials and code.
1. Request received over TLS
2. Credentials deserialized into memory (never written to disk)
3. Used for signing/uploading during build
4. After build completes (success or failure):
a. Credential memory explicitly zeroed (zeroize crate)
b. Credential variables dropped
5. No logs contain credential material
6. No crash dumps include credential memory (configured at OS level)
1. Tarball extracted to /tmp/perry-build-{uuid}/
2. Compilation happens in this directory
3. After build completes (success or failure):
a. rm -rf /tmp/perry-build-{uuid}/
b. Verified deletion
4. No source code in logs (only filenames)
5. Artifacts deleted after download or 10-minute TTL
| Tier | Concurrent builds | Builds per hour | Max project size |
|---|---|---|---|
| Free | 1 | 5 | 50 MB |
| Pro | 3 | 30 | 200 MB |
| Self-hosted | Unlimited | Unlimited | Unlimited |
| Feature | Free | Pro (€29/mo) |
|---|---|---|
| Perry compiler | ✓ | ✓ |
perry build (local) |
✓ | ✓ |
perry publish (1 platform) |
✓ | ✓ |
perry publish (multi-platform) |
✗ | ✓ |
| Build server usage | ✓ (rate limited) | ✓ (higher limits) |
| Priority queue | ✗ | ✓ |
| Email support | Community | Direct |
| Self-hosted server | ✓ | ✓ |
FREE-XXXX-XXXX-XXXX (free tier, auto-generated)
PRO-XXXX-XXXX-XXXX (paid tier, via Stripe/Polar.sh)
- Polar.sh or Lemon Squeezy for payment processing
- Handles EU VAT (important for Skelpo GmbH)
- Simple checkout page at perry.dev/pro
- License key delivered instantly via email + shown in CLI
- No recurring billing infrastructure needed on our side
Every error message follows this structure:
✗ {What went wrong}
{Why it went wrong — one sentence}
{Exact next step — specific command or URL}
✗ Signing failed
Your App Store Connect API key has expired.
Generate a new one at:
https://appstoreconnect.apple.com/access/api
Then run: perry auth apple
✗ Compilation failed
Type error in src/api.ts:42
Property 'name' does not exist on type 'User'.
Fix the error and run: perry publish --ios
✗ Bundle ID mismatch
perry.toml says "com.dev.myapp" but your provisioning
profile is for "com.developer.myapp".
Either:
• Update bundle_id in perry.toml to "com.developer.myapp"
• Create a new provisioning profile at:
https://developer.apple.com/account/resources/profiles
✗ Upload to Play Store failed
Your app's version code (1) is not higher than the
current live version (1).
Bump version in perry.toml:
version = "1.0.1"
✗ Notarization rejected by Apple
Apple's response: "The binary uses a private API (IOKit)."
This usually means a dependency is using restricted APIs.
Check: https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution
For common issues that assume zero knowledge:
✗ No Apple Developer account found
To publish iOS apps, you need an Apple Developer account ($99/year).
1. Sign up at https://developer.apple.com/programs/
2. Wait for approval (usually 24-48 hours)
3. Then run: perry auth apple
💡 Tip: You can test locally with: perry build --ios --simulator
✗ No signing keystore found
Android apps must be signed with a keystore before publishing.
Perry can create one for you.
Run: perry auth android --create-keystore
⚠ IMPORTANT: Back up the keystore file! If you lose it,
you cannot update your app on the Play Store.
perry publish [OPTIONS]
TARGETS (at least one required):
--macos Build and publish for macOS
--ios Build and publish for iOS
--android Build and publish for Android
--windows Build and publish for Windows
--linux Build and publish for Linux
--all Build and publish for all configured targets
OPTIONS:
--local Build locally instead of using build service
--no-upload Build and sign but don't upload to store
--dry-run Validate config and credentials without building
--verbose Show detailed build output
--config <path> Path to perry.toml (default: ./perry.toml)
AUTHENTICATION:
perry auth apple Set up App Store Connect credentials
perry auth google Set up Google Play credentials
perry auth windows Set up Windows signing certificate
perry auth android Set up Android keystore
--create-keystore Create a new signing keystore
perry auth github Authenticate with GitHub (for license)
perry auth check Verify all configured credentials
ACCOUNT:
perry account Show license status and usage
perry account upgrade Open upgrade page
VERSION MANAGEMENT:
perry version bump <major|minor|patch>
Bump version in perry.toml
perry version set <version>
Set specific version in perry.toml
-
perry publishsubcommand scaffolding in existing CLI -
perry.tomlparsing for publish configuration -
~/.perry/credentials storage with encryption -
perry auth githubdevice flow for license registration - License key generation API (simple Rust HTTP server)
- .app bundle generation from compiled binary
- Info.plist generation from perry.toml
- Icon generation (all sizes from single PNG, using
imagecrate) -
codesignintegration -
notarytoolintegration (or App Store Connect API for notarization) - DMG creation via
hdiutil - Build server: accept tarball, compile, sign, return artifact
- WebSocket progress streaming
-
perry publish --macosend-to-end working
- iOS .app bundle structure generation
- Asset catalog generation (investigate pure Rust vs actool)
- Provisioning profile embedding
- iOS code signing
- .ipa packaging
- App Store Connect upload API integration
- Guided provisioning profile creation flow in CLI
-
perry publish --iosend-to-end working
- AndroidManifest.xml generation
- APK/AAB structure creation
- Android icon generation (all density buckets, adaptive icons)
- apksigner integration
- Google Play Developer API upload integration
-
perry auth android --create-keystore -
perry publish --androidend-to-end working
- Windows EXE resource embedding (icon, version info)
- osslsigncode integration for Windows signing
- MSIX packaging (stretch goal)
- AppImage creation for Linux
- .deb packaging for Linux
-
perry publish --windowsand--linuxworking
- Multi-platform parallel builds
-
perry publish --all - Payment integration (Polar.sh / Lemon Squeezy)
- perry.dev website with docs and pricing
- Error message review and vibe-coder testing
- Security audit of credential handling
- Open source perry-build-server
- Launch announcement
-
Asset catalog compilation without Xcode: Can we generate
Assets.carin pure Rust? If not, Xcode Command Line Tools (~2GB) are needed on the build server. This is fine for our server but blocks fully Xcode-free local builds. -
iOS Simulator builds: Should
perry build --ios --simulatorwork locally without any Apple credentials for testing? This would be great for the development loop. -
Provisioning profile automation: Can we fully automate provisioning profile creation via the App Store Connect API, or do users need to manually create them? Investigate API capabilities.
-
Windows cross-compilation: How mature is CraneLift's PE/COFF output? May need LLVM backend for Windows targets initially.
-
App Store metadata: Should perry.toml include store listing metadata (description, screenshots, keywords) or is that out of scope for v1? Leaning toward out of scope — stores have their own dashboards for this.
-
CI/CD integration: Should we provide GitHub Actions / GitLab CI templates for
perry publish? Probably yes, as a follow-up after launch. -
Version management: Auto-increment build numbers for stores that require unique build numbers per upload (iOS especially)?
-
Perry compiler version pinning: Should the build server support building with specific Perry versions, or always use latest? Probably pin to project's
perry_versionin manifest.
| Tool | What it does | Perry Publish advantage |
|---|---|---|
| Fastlane | Ruby-based mobile deployment automation | No Ruby dependency, integrated with compiler, simpler config |
| Xcode | Full Apple IDE and build system | Not needed at all, massive footprint reduction |
| Android Studio | Full Android IDE and build system | Not needed at all |
| Expo EAS | React Native cloud builds | Perry produces true native binaries, not JS bundles |
| Capacitor | Web-to-native wrapper | Perry compiles to native, no WebView |
| Flutter | Cross-platform framework | TypeScript instead of Dart, single toolchain |
| Codemagic | Mobile CI/CD service | Perry owns the whole pipeline from source to store |
Perry Publish's unique position: only tool that owns the entire pipeline from TypeScript source code to app store listing, with no IDE dependencies.
This is a living document. Last updated: February 25, 2026.