I built an iPhone app without owning a Mac. Then I made it run on Android too. The whole thing took about nine days, and most of it happened on Linux servers I talk to from my phone.

That sentence would have sounded like a scam a couple of years ago. Now it’s just how my week went.

What CaptionCam does

CaptionCam is a small app I wanted for myself. You record or import a video, it transcribes the speech on the phone, you fix the captions, and it exports an MP4 with the subtitles burned into the picture. The ones with the yellow word that follows along as you talk, like every other short video out there.

The important part for me: everything stays on the device. No backend, no account, no uploading my videos to someone’s server just to get subtitles.

Under the hood it’s an Expo app (SDK 58, React Native, TypeScript) with native modules written in Swift for iOS and Kotlin for Android:

  • Transcription: Apple’s SpeechAnalyzer, or whisper.cpp running locally (Metal on iPhone, CPU on Android). Whisper is the default because it handles Malay and our proper rojak Manglish way better.
  • Caption editor: 3 to 5 words per caption, with editable words, timing, size, outline, shadow and position, plus a few style presets.
  • Export: 720p, 1080p or 4K HEVC. If the source is HDR or Dolby Vision, the export keeps it.
  • Camera: a native camera with every lens, 4K and 60 fps.
  • Optional Apple Intelligence: tidies English punctuation and suggests caption breaks. It’s off by default, and every suggestion gets checked word by word against the original.

It’s a personal app, installed on my own iPhone 15 Pro. Not going to the App Store, at least not now.

How it actually got built

The first commit was on 10 October. By 18 October the repo had 75 commits and roughly 23,000 lines across TypeScript, Swift and Kotlin. Some of the checkpoints along the way:

  • 10 Oct: the full plan, phases 1 to 6 (record, transcribe, edit, export, Whisper, Apple Intelligence), plus the free cloud build route.
  • 11 Oct: first builds on the phone, and the fun bugs. Captions that were completely invisible. An API that only exists on iOS 27 sneaking into an iOS 26 build. In the end I just moved the app to iOS 27 minimum. Same day: multilingual Whisper, 4K export and a native camera.
  • 12 Oct: the camera fights back. The preview showed nothing because it was laid out with zero height. The front camera crashed with HDR on. Classic. Also app icons, and HDR and Dolby Vision export.
  • 14 Oct: the whole Android port in one day: Whisper, caption rendering, export with Media3, a CameraX camera, and an APK build workflow.
  • 15 to 18 Oct: bug fixes, the upgrade to Expo SDK 58, and switching the UI to @expo/ui so the controls are real SwiftUI on iOS and Material 3 on Android instead of hand-made buttons pretending to be native.

I didn’t type most of that code. Agents did, through T3 Code, running on my servers. My job was the boring but important part: writing down what I wanted, reading the changes, installing the build on my phone, and coming back with “the captions are gone” or “the camera is black”.

The repo has an AGENTS.md that tells the agent how to behave. The main rule is basically: don’t trust your memory. Expo changes between SDKs and Apple changes its APIs every year, so check the versioned docs or the real SDK before using anything. That one rule saved me a lot of nonsense.

The other rule: the Swift code was written and type-checked on Linux. No Xcode on my side, ever. Which brings us to the Mac problem.

The Mac problem, and ipa-builder

Normally, to put your own iOS app on your own iPhone, you need a Mac with Xcode, or $99 a year for an Apple Developer account. Just to install your own app on your own phone. Sheesh.

Sideloadly and AltStore can sign and install an app with a free Apple ID. But they need a compiled .ipa first, and compiling an iOS app means Xcode, which means a Mac.

ipa-builder by adro solves the compiling part. It’s a GitHub Actions workflow that checks out your app, builds it on GitHub’s macOS runners with code signing turned off, and hands you back an unsigned .ipa. Then Sideloadly signs it with your free Apple ID and puts it on your phone. Very clever, and MIT licensed.

So I forked it. The original worked great for simpler Expo apps, but CaptionCam has five custom native modules and wanted the newest Xcode, so I needed a few more things:

  • Newer Xcode: support for Xcode 26, then a runner input so I can pick xcode-27 (macOS 27, iOS 27 SDK). That’s now the default.
  • Native module apps: npm ci when there’s a lockfile, expo prebuild then pod install, a quiet xcodebuild, and the full build log uploaded as an artifact when it fails.
  • Inputs: node_version, xcode_version and extra_env, with Node 26 as the default.
  • Encryption: an optional IPA_PASSPHRASE. Logs and artifacts of a public repo are public, so this encrypts the .ipa and the build log before upload.
  • An SDK inspector: a small read-only workflow that prints API declarations straight from the Xcode SDK.

That last one is my favourite. The agent can’t open Xcode, but it can trigger a workflow on a real Mac in GitHub’s cloud and read the actual Swift headers for the iOS 27 SDK. So when it says an API exists, it has checked.

Now my build flow is: the agent pushes a branch, triggers the build, and sends me the run link. When it’s green, I download the .ipa, decrypt it, drag it into Sideloadly, and it’s on my phone. Android is even simpler: a workflow builds the APK and I install it directly.

Some catches, so nobody says I didn’t warn them:

  • The 7-day limit: free Apple ID signatures expire after 7 days, so you re-sign every week (AltStore can refresh automatically).
  • Queues: the xcode-27 runner is a GitHub preview image, so sometimes you wait in a queue.
  • Minutes: macOS minutes on a private repo are billed at 10x against the free allowance. A public builder repo with the passphrase set avoids that.
  • Android testing: Android is behind iOS. It builds, but I’ve done much less testing there than on the iPhone.

Ridiculous, in a good way

Let me list what I didn’t need for this project: a Mac, Xcode, or a paid Apple account.

What I did need: a phone, my Linux servers, a GitHub account, and agents that can read docs, write Swift and Kotlin, and press the build button for me.

Expo is a big part of why this works. One codebase for both platforms, native modules when you need real platform features, and config plugins so I never edit the generated ios/ or android/ folders. GitHub Actions gives me a Mac by the minute. T3 Code ties it together and lets me drive all of it from wherever I am, including from my phone.

It still isn’t magic. The invisible captions, the black camera preview and the HDR crash were all found by me, holding the phone, testing things. The agents write the code fast. Making sure it actually works on a real device is still my job, and I think that’s the right split.

But the barrier to building a real native app, with on-device AI, a custom camera and 4K HDR export, for both platforms, as one guy with a day job and a toddler? That barrier is basically gone. Ridiculous. I love it.