iOS on the Platform
Building for iOS normally means owning a Mac: only Xcode can archive an iOS app and only Xcode can hand it to App Store Connect. On the Platform, both happen on a hosted macOS runner, so the machine you type on can be Linux, Windows, or a CI job.
The app itself is the same Hotwire Native shell as the DIY track: a thin native wrapper around your deployed site.
Before you start
- Your app is deployed and in remote mode. iOS builds are remote mode only:
a phone cannot boot a compiled Rails server.
mode: remoteplus an absolutehttps://remote.urlis required, and the Platform refuses the submission without it. - You are in the Apple Developer Program ($99 a year). The Platform cannot issue certificates on your behalf.
- You have connected the CLI with
every platform login.
app:
name: Notes
bundle_id: com.example.notes
version: "1.4.0"
mode: remote
remote:
url: https://notes.example.com
build:
targets:
- ios-arm64:testflight
platforms:
ios:
bundle_id: com.example.notes.ios # optional; the App Store often wants its own id
Whichever bundle id iOS ends up with is the one you register with Apple and the one your provisioning profile must cover.
Upload three credentials
TestFlight and the App Store need the same three, all under Credentials → New. None of them requires a Mac.
1. Apple Distribution certificate (.p12)
If you have one, export it with its password and upload it. If you do not, and
you have no Mac to create one, use the Platform's Mac-less flow: it
generates the keypair and a CSR on the server, you upload the CSR to Apple
Developer, and you upload Apple's issued .cer back. See
credentials.
2. Provisioning profile (.mobileprovision)
Entirely web-based:
- Register your iOS bundle id in Apple Developer → Identifiers.
- In Apple Developer → Profiles, create an App Store Connect distribution profile for that bundle id, choosing the same Apple Distribution certificate you uploaded.
- Download the
.mobileprovisionand upload it. The Platform reads its name, team, app id, and expiry, and refuses an expired one or one whose team doesn't match what you entered.
3. App Store Connect API key (.p8)
In App Store Connect → Users and Access → Integrations, generate a Team
Key with the Developer role or higher and download the .p8. Apple lets you
download it exactly once. Upload it with its Key ID, in the key's row, and
Issuer ID, at the top of that page.
This key is what the runner uses to upload your build to App Store Connect.
The Credentials page has an "iOS · TestFlight and the App Store" checklist that turns green when all three are verified.
Build it
every platform build --target ios-arm64:testflight
Use ios-arm64:app_store for an App Store submission build; everything below is
the same for both. With the target already in build.targets, plain
every platform build picks it up.
The build waits for a hosted macOS runner, then archives, signs with your
distribution certificate and profile, uploads the .ipa to App Store Connect,
and exports a second copy of the .ipa into dist/ for your records. You do
not install that file directly: iOS apps reach devices through App Store
Connect.
Bump the version for every upload. The shell stamps a fixed build number of
1, so App Store Connect tells your uploads apart byapp.version(orplatforms.ios.version) alone. Re-uploading the same version is rejected on Apple's side.
After the build
- App Store Connect processes the upload. Usually minutes, sometimes longer. The build appears in the app record's TestFlight tab when it is done.
- Testers. Internal testers can install it as soon as processing finishes; external testing needs Apple's beta review first. Both are done in App Store Connect.
- Promote it in the dashboard. Open the succeeded build, press Promote,
and pick a channel. For iOS this records the release against the version you
shipped. The upload already happened on the runner, so promoting moves
nothing along in App Store Connect. Submitting an
app_storebuild for review stays a manual step there.
Promotion is refused if the build never uploaded. That means the API key was missing or wrong when the runner ran; add it and build again.
When a build is refused
- "iOS builds are remote mode only". Set
mode: remoteineverywhere.yml. - "remote mode needs remote.url set to an absolute http(s) URL". A relative
or empty
remote.url. The shell has nothing to point at without it. - Missing credentials. The error names which of the three are missing or unverified, and links to the credentials page.
- Unsupported target.
ios-arm64:directdoes not exist. iOS ships through App Store Connect, so the channel must betestflightorapp_store.
What's the same as DIY
Tabs, icons, colors, native extensions, and the
bridge all behave as they do on the
DIY iOS page. The Platform changes where the build runs, not
what gets built. Local development still uses every dev --ios on a Mac.