Browse documentation
Documentation for v0.14.0View original source ↗

Soundsible for iOS

Native client code for your own Soundsible server.

Never installed or run on a device. Nobody has installed this iOS app anywhere. Its runtime behaviour is entirely theoretical. CI produces an IPA and automated tests cover parts of the code, but neither proves installation, pairing, playback, background audio, offline listening or car integration. The instructions and feature descriptions below describe the intended behaviour of the code, not a verified user experience. First installation and physical-device validation are still required.

The intended capabilities include background playback, downloading music from its server for disconnected listening, and native media metadata and controls.

It is not on the App Store, and that is deliberate. See Why not the App Store.


Installing

This procedure has never been executed for Soundsible.

You need an iPhone or iPad on iOS 26 or newer and a sideloading app. SideStore is the recommended one: after a one-time setup it re-signs your apps on the phone itself, with no computer.

  1. Install SideStore following its own instructions. This is the only step that needs a computer: SideStore is set up once from a desktop, and after that it re-signs your apps on the phone itself.

  2. Add the Soundsible source:

    https://github.com/Arzuparreta/soundsible/releases/latest/download/apps.json

    That URL always resolves to the newest release, so the app updates itself from it.

  3. Install Soundsible from that source.

  4. Open it and pair with your server (below).

Release downloads. Published releases include apps.json and Soundsible.ipa. Use the source above to install the released app; workflow artifacts are development builds. This does not verify the separate AltStore PAL distribution path described below.

The seven-day thing. A free Apple ID can sign at most 3 sideloaded apps, and the signature lasts 7 days. SideStore renews it in the background on the phone, and pairing it with LiveContainer works around the three-app cap. Both are Apple’s limits on sideloading, not something Soundsible can lift.

You can also grab the .ipa straight from a release and install it with AltStore, SideStore or Sideloadly.

Pairing

On your Soundsible, open Settings → Pair a device and leave the QR sheet on screen. In the app, tap Scan the pairing code.

Keeping the sheet open matters: it is what turns on auto-confirm, and auto-confirm is what lets the phone finish pairing on its own. If the sheet is closed the engine accepts the code but waits for you to confirm on the server — and the token it mints then goes to whoever confirmed, never to the phone. The app says so rather than spinning forever.

For a headless server, use I already have a device token and paste a paired-device token with the library:read scope.

The credential lives in the Keychain and is only ever sent to your own server.

In the car

The intended flow is to connect through Bluetooth, USB or CarPlay and start playback from the phone. None of the following has been tested on a device.

Implementation intent:

Title, artist, album and artwork on the head unitImplemented in code; unverified
Progress bar that tracks the songImplemented in code; unverified
Play, pause, next, previous from the wheel or dashboardImplemented in code; unverified
Soundsible’s Now Playing screen inside CarPlayImplemented in code; unverified
Browsing your library from the car screen❌

That last one is a CarPlay app, which needs the carplay-audio entitlement. Apple grants it only to apps published on the App Store, so it is out of reach on this distribution route. Everything above it needs no entitlement at all — it is MPNowPlayingInfoCenter and MPRemoteCommandCenter, the same system APIs that drive the lock screen.

Offline

The following workflow exists in code but has never been exercised on a device.

Open a playlist, album or Favourites and choose Make available offline. Only what is missing is fetched, so pinning two playlists that overlap costs the difference and not the sum.

Settings → Offline sets a storage limit. Anything you pinned explicitly is never evicted; only music that was downloaded on the way past is, least recently played first.

Crossfade and DJ

Settings → Playback sets a crossfade of up to 12 seconds. Halfway through the blend the lock screen and the car switch to the incoming track — that handover point is the thing a browser cannot control, because the Media Session API only describes one track at a time while two are sounding.


Why not the App Store

Guideline 5.2.3 forbids apps that “save, convert, or download media from third-party sources (e.g. Apple Music, YouTube, SoundCloud, Vimeo, etc.) without explicit authorization from those sources”. Acquiring music from YouTube is what Soundsible is, so the only version Apple would approve is one with the feature removed — a library player, and not this project.

The trade is honest and worth stating plainly:

App StoreSideloading
Apple Developer Program99 €/yearNot needed
The app can be itselfNoYes
InstallOne tapSideStore, and a 7-day renewal
CarPlay browse screenPossibleNo

The bottom row is not the whole story: AltStore PAL buys back most of what sideloading costs, without cutting anything out of the app. It is prepared and waiting — see below.

The paid upgrade: AltStore PAL

Everything for this is already in the repository, dormant. It is written down here so the day it gets paid for is an afternoon and not a research project.

What it buys

Sideloading (today)AltStore PAL
Cost to the developernothing99 €/year
Computer needed to installonce, to set up SideStorenever — the marketplace installs from Safari
Signatureexpires every 7 dayspermanent
Three-app capyesno
Where it workseverywhereEU, Japan, Brazil
Does the app have to be cut down?nono — Notarization reviews security, not content

The app is free and does not monetise, so nothing beyond the membership is owed: Apple’s Core Technology Fee only starts above a million first annual installs.

Where you are

python3 scripts/altstore_pal_preflight.py

It prints the whole checklist with what is done and what is next. Steps that happen in a browser leave nothing to detect, so record those by hand:

python3 scripts/altstore_pal_preflight.py --confirm membership

The order

  1. Join the Apple Developer Program as an individual — no D-U-N-S, verified in a day or two. Under the EU’s Digital Services Act your name, address, phone and email are published on your listing, and a PO box is accepted for the address.

  2. Request the Alternative Terms Addendum for Apps in the EU in the developer portal. Without it there is no alternative distribution at all.

  3. Register your Developer ID with AltStore PAL through their REST API. It answers with a security token.

  4. App Store Connect → Users and Access → Integrations → Marketplace → +, and paste that token. Then pick Soundsible as an app to distribute.

  5. Store the credentials the workflow needs — the preflight names all six secrets and three variables, with what each one is.

  6. Run the iOS AltStore PAL workflow. It builds a signed archive and uploads it to App Store Connect.

  7. App Store Connect → the version → App Review Information → Review Type → Notarization. Save, Add for Review, Submit to App Review. This is the step that matters: reviewed against the Notarization Review Guidelines, the app is judged on security and integrity, not on where its music comes from.

  8. On acceptance Apple generates the Alternative Distribution Package by itself.

  9. Collect the ADP through AltStore PAL’s REST API and host it with its directory structure and file hashes intact. A GitHub release asset cannot do that — it is a flat file. GitHub Pages on soundsible.github.io can.

  10. Generate the PAL source and publish it alongside the sideloading one:

    python3 scripts/altstore_source.py \
      --ipa <the signed ipa> \
      --marketplace-id "$SOUNDSIBLE_MARKETPLACE_ID" \
      --download-url "$SOUNDSIBLE_ADP_BASE_URL" \
      --out apps-pal.json

Both sources keep the same bundle identifier, so PAL upgrades an existing sideloaded install rather than putting a second copy beside it.

What is verified, and what is not

This matters more than it looks. The PAL files sit in the repository next to code that CI exercises on every push, and nothing about their appearance distinguishes them. Anyone — a person or an agent — reading ios-altstore-pal.yml could reasonably assume it works. It does not, because it has never been given the chance.

Status
ios-build.yml, IPA generationBuild only verified. Produces a real IPA; installation and runtime have never been tested.
scripts/altstore_source.py, sideloading halfGeneration only verified. Runs on releases and has tests; nobody has installed the app from its source.
scripts/altstore_source.py, --marketplace-id halfField names from documentation; has tests for its shape, but no client has ever installed from a source it produced.
scripts/altstore_pal_preflight.pyVerified. Has tests and runs. It is a checklist for something unverified.
.github/workflows/ios-altstore-pal.ymlNever executed. Not once.
ios/exportOptions/app-store-connect.plistNever used.

CI passing says nothing about the last two: nothing in CI runs them.

Where the PAL steps came from

All read 2026-08-06. Apple and AltStore both move these pages, so re-check before relying on any of it.

  1. Distribute with AltStore PAL — the ordered steps, the Developer ID registration through their REST API, the App Store Connect marketplace token, and the requirement to host the ADP with its directory structure and file hashes intact.
  2. Make a Source — the JSON schema, and that marketplaceID is required only for notarized apps in PAL.
  3. Submit for Notarization — that the build is uploaded like any other, that the difference is the Review Type, and that Apple generates the ADP itself on acceptance.
  4. DMA and apps in the EU — that Notarization reviews security and integrity rather than content, that an individual developer needs no organisation, and that the Core Technology Fee starts above a million first annual installs.

The guesses, in the order they will probably bite

  • method: app-store-connect in the export plist. The method list found while researching was app-store, ad-hoc, package, enterprise, development, developer-id, mac-application — with no app-store-connect in it. Recent Xcode renamed app-store to app-store-connect; which one Xcode 26 wants was never confirmed. If the export complains about the method, try app-store.
  • xcrun altool --upload-app. Documented for App Store Connect uploads, but altool was deprecated in favour of notarytool on macOS and may be gone or changed in Xcode 26. Fallbacks: xcrun notarytool, or an exportOptions destination: upload with -authenticationKeyPath.
  • ~/.appstoreconnect/private_keys/AuthKey_<KEYID>.p8. altool’s documented lookup path, and moot if the point above changes.
  • CODE_SIGN_IDENTITY="Apple Distribution". The exact string depends on what the certificate is called. security find-identity -v -p codesigning on the runner will say.
  • Whether step 9 is needed at all. AltStore’s documentation mentions PAL processing builds automatically, which may make collecting the ADP by hand unnecessary.

Building it yourself

You do not need a Mac to build the parts that hold the logic. ios/ is split so that everything except the SwiftUI and AVFoundation shell is plain Swift:

# Core library — API client, queue, offline policy. Runs anywhere.
docker run --rm -v "$PWD/ios/SoundsibleKit":/w -w /w swift:6.3 swift test

The app shell needs Xcode, and the iOS GitHub Actions workflow is where that happens — macOS runners are free and unmetered for public repositories. It generates the Xcode project with XcodeGen from ios/project.yml, builds an unsigned archive and attaches the .ipa as an artefact.

The .xcodeproj is generated, never committed: nobody on this project owns a Mac to edit one with, so a committed project file would be a file no contributor could change.

On a Mac:

brew install xcodegen
cd ios && xcodegen generate && open Soundsible.xcodeproj

Layout

PathWhat it is
ios/SoundsibleKit/Models, API client, pairing, PlayQueue, OfflineLibrary. No Apple frameworks; tested on Linux.
ios/App/Audio/AVAudioSession, the two decks, Now Playing, remote commands, the authenticated stream loader.
ios/App/Model/AppModel (paired or not) and PlayerModel (what is sounding).
ios/App/Views/Pairing, browse, Now Playing, settings.
ios/project.ymlThe Xcode project, as YAML.

The app targets iOS 26 and is built against the iOS 26 SDK, so it adopts Liquid Glass rather than opting out with UIDesignRequiresCompatibility — an escape hatch Apple removes in Xcode 27 anyway. The app target runs under the Swift 6 language mode with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, Xcode 26’s default for a new project; SoundsibleKit deliberately stays nonisolated because it is the part that does work off the main thread.

What it asks of the engine

Only what already exists — see CAR_INTEGRATION.md:

  • GET /api/car/home, GET /api/car/items/<id> for browsing
  • GET /api/static/stream/<track_id>, GET /api/static/cover/<track_id>
  • POST /api/pairing/sessions/claim, GET /api/pairing/verify
  • POST /api/devices/register, PUT /api/playback/state

Everything carries Authorization: Bearer <paired-device token>.

Search documentation