I can't write code.
That's not quite right. I describe what I want, Claude writes the code, and I focus on what to build. I'm what people call a VibeCoder — someone who builds products with AI, spending their time on product decisions rather than syntax.
I'm building "Arc," a mobile CRM for bodywork therapists, using Expo, React Native, and Supabase. The MVP is 85% done — 28 files, 27 screens, 18 of them functional.
The problem wasn't building it. The problem was getting it onto a tester's phone.
The last mile is the hardest mile
Writing code is fast. Tell Claude "build this screen" and something functional appears in minutes.
But putting that build on a tester's iPhone requires:
- Building with Xcode
- Generating an .ipa file
- Uploading it somewhere
- Telling testers "there's a new version"
For a VibeCoder, this "last mile" is the biggest bottleneck — not writing code, but delivering what you've written.
TestFlight? Apple's review queue adds hours or days. Expo Go? Can't use native modules. EAS Build (cloud)? Monthly subscription for a solo dev feels wrong.
The answer turned out to be DeployGate + a self-hosted GitHub Actions runner on my 13-inch M5 MacBook Air. Push to main, wait ~10 minutes, and every tester gets the latest build at a fixed URL. $0/month. Unlimited builds.
What I wanted
Simple, right? Here's what actually happened — 11 failures before attempt #12 finally worked.
Attempt #1 — Workflow collision
Error: Build failed at 3:54 — crashed on "Embed Pods Frameworks."
Cause: Two workflows triggered on the same push to main — one for DeployGate, one for TestFlight. My Mac's Xcode resources couldn't handle both simultaneously.
Fix: Moved the TestFlight workflow out of the active directory.
mkdir -p .github/workflows-disabled
mv .github/workflows/build-and-submit.yml .github/workflows-disabled/
Attempts #2–3 — Binary files in the repo
Error: Every push uploaded 15MB of binary data.
Cause: A .ipa file and an Apple certificate had been accidentally committed.
Fix: Added them to .gitignore and purged from history.
build-*.ipa
AppleWWDRCAG3.cer
Lesson: build artifacts never belong in a repository.
Attempts #4–5 — Package drift
Error: Same "Embed Pods Frameworks" crash.
Cause: Between sessions, react-native had silently updated from 0.83.2 to 0.83.4, and react-native-worklets appeared as a new peer dependency of react-native-reanimated.
How I found it: Checked out the last known working commit's package.json and rebuilt.
git checkout 89fcbe9 -- package.json package-lock.json
npm ci
eas build --platform ios --profile preview --local
# → Build successful
Fix: Pinned packages to the stable version, then formally added the missing dependency.
Attempts #6–7 — Stale runner cache
Error: Builds pass locally, fail in CI.
Cause: The self-hosted runner's working directory had stale node_modules and Xcode DerivedData from previous runs.
Fix: Added a cache-clearing step to the workflow.
- name: Clean caches
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/Arc-*
rm -rf /tmp/eas-build-local-nodejs
rm -rf /tmp/eas-cli-nodejs
Attempts #8–9 — Credentials source ignored
Error: Console showed Using remote iOS credentials (Expo server) when I wanted local credentials.
Cause: The EAS_CREDENTIALS_SOURCE=local environment variable was silently ignored by eas-cli 18.5.0.
Fix: Set it directly in eas.json.
"preview": {
"distribution": "internal",
"credentialsSource": "local"
}
Attempt #10 — Missing credentials file
Error: credentials.json does not exist in the project root directory
Cause: credentials.json is in .gitignore (as it should be), so the runner's checkout didn't include it.
Fix: Copied it from the Mac's home directory in the workflow.
- name: Copy credentials
run: |
cp ~/client-crm/credentials.json ./credentials.json
cp -r ~/client-crm/credentials ./credentials
Attempt #11 — Locked keychain
Error: Everything compiled fine. Failed only at the final code-signing step ("Embed Pods Frameworks").
Cause: The self-hosted runner runs as a LaunchAgent without access to the GUI session's keychain.
Fix: Explicitly unlock the keychain in the workflow.
- name: Unlock keychain
run: security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" ~/Library/Keychains/login.keychain-db
Store your Mac login password in GitHub Secrets as KEYCHAIN_PASSWORD.
Attempt #12 — It worked 🎉
✔ Using local iOS credentials (credentials.json)
✔ Archive Succeeded
✔ Successfully exported and signed the ipa file
✔ Uploading to DeployGate...
✔ Distribution page updated
From git push to a tester opening the build on their phone: about 10 minutes.
The final workflow
Here's the complete GitHub Actions workflow that survived all 11 failures:
name: Build & Deploy to DeployGate
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Clean caches
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/Arc-*
rm -rf /tmp/eas-build-local-nodejs
rm -rf /tmp/eas-cli-nodejs
- name: Install dependencies
run: npm ci
- name: Copy credentials
run: |
cp ~/client-crm/credentials.json ./credentials.json
cp -r ~/client-crm/credentials ./credentials
- name: Unlock keychain
run: security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" ~/Library/Keychains/login.keychain-db
- name: Build .ipa
run: eas build --platform ios --profile preview --local --non-interactive
- name: Upload to DeployGate & update distribution page
run: |
IPA_FILE=$(ls build-*.ipa | head -1)
curl -X POST \
"https://deploygate.com/api/users/${DEPLOYGATE_OWNER}/apps" \
-H "Authorization: Bearer ${DEPLOYGATE_API_TOKEN}" \
-F "file=@${IPA_FILE}" \
--form-string "distribution_key=${DEPLOYGATE_DIST_KEY}" \
--form-string "release_note=$(git log -1 --pretty=%B)"
env:
DEPLOYGATE_OWNER: ${{ secrets.DEPLOYGATE_OWNER }}
DEPLOYGATE_API_TOKEN: ${{ secrets.DEPLOYGATE_API_TOKEN }}
DEPLOYGATE_DIST_KEY: ${{ secrets.DEPLOYGATE_DIST_KEY }}
The DeployGate upload API accepts both the .ipa file and distribution page parameters in a single call — pass distribution_key to update an existing distribution page and release_note to auto-populate release notes from your commit message. Testers access the same fixed URL every time. No new links to send, no app store to check.
Why DeployGate instead of alternatives?
I tried the obvious options first:
TestFlight: Apple's review queue for the first build of each version can take hours to days. When you're iterating multiple times per day, that's a dealbreaker.
EAS Build (cloud): Works great, but the monthly cost adds up for a solo developer building across multiple projects.
Expo Go: Fine for early prototyping, but the moment you add native modules (and you will), it stops working.
DeployGate fit because: no review queue, free tier for indie devs, and an API simple enough to add to a GitHub Actions workflow in one curl command. Testers don't need accounts — they open a link and install.
What I learned
After 11 failures, every lesson is burned in:
One workflow per trigger. Two workflows on the samepushwith shared Xcode resources will collide.
Pin your packages.npm cionly protects you ifpackage-lock.jsonhasn't drifted.
Self-hosted runners need manual cache hygiene. Unlike cloud runners, they accumulate state between builds.
eas.jsonover environment variables. Some CLI tools silently ignore env vars for credential configuration.
Keychain access is not automatic. LaunchAgent processes can't see your GUI keychain without explicit unlock.
Keep credentials out of the repo, but script their placement. Copy them from a known location on the build machine.
I'm a solo developer building multiple products — a CRM for bodywork therapists, a learning tool for structural consultants, and a few things I can't talk about yet.
Making mistakes along the way and sharing all of it here. Follow @kazbuildsstaff for real build-in-public, not the highlight reel.



SOCIAL SHARE CARD GENERATOR