All work
GodotClient Delivered2026

Desvelado on iOS

Took an already released Godot game to iOS: a signed build on TestFlight straight out of cloud CI, a lives economy the client can retune from Firestore, and the packaging fix that finally made Firebase cloud saves work on a real phone.

Out of lives, mid level. The player is offered a rewarded ad for an instant full refill, or can wait out the free one; the countdown underneath is live and keeps running with the app closed.
Out of lives, mid level. The player is offered a rewarded ad for an instant full refill, or can wait out the free one; the countdown underneath is live and keeps running with the app closed.
Client
Fiverr (@boronatic)
Role
Release and Systems Programmer
Platforms
iOS, Android
Year
2026

Desvelado is the client studio's own game, already out on Steam and Android. I was hired for the ring around it rather than the game itself: getting an iOS build out of Godot and onto TestFlight, adding a lives economy the client could tune without shipping a build, and working out why Firebase, in app purchases and ads all behaved in the editor but not on a real device.

Godot cannot produce a signed .ipa without a Mac, so the build runs on Codemagic. The workflow pulls Godot 4.6.3 and its export templates onto a mac mini runner, imports the project headless, exports an Xcode project, runs CocoaPods when Godot emits a Podfile, fetches signing files through an App Store Connect API key, builds a signed .ipa and uploads it to TestFlight. Nothing secret is committed: the .p8 key and a generated RSA key live in a secure Codemagic variable group, and the workflow creates or fetches the distribution certificate itself.

The most valuable hour on the job went into proving where Firebase actually broke. In the editor it signed in against the client's live project and completed a full save round trip. On the phone it printed one line: "Unable to read Firebase config". The addon reads its credentials from a plain .env file, and Godot's exporter only packs files it recognises as resources, so that file never made it into the build. No sign in meant no cloud save, which is exactly why progress looked like it reset on device, on both Android and iOS. Renaming it to firebase.env and adding *.env to the non resource export filters on both presets fixed it without a line of code changing.

The lives system is the one piece of new gameplay. A life is spent on each death; at zero the game offers a rewarded ad that refills to full or a trip back to the menu. Lives also come back on their own against a wall clock deadline written into the save, so the timer keeps running while the app is closed and is checked again the moment the app returns to the foreground. Maximum lives, the refill interval and whether the system is on at all are read from Firestore, so the economy is retuned from a console rather than a release.

What I built

  • Built the iOS release pipeline in codemagic.yaml: Godot 4.6.3 and export templates fetched onto a mac mini runner, headless import, export to an Xcode project, CocoaPods when needed, automatic signing through an App Store Connect API key, signed .ipa uploaded to TestFlight
  • Kept every credential out of the repository: the App Store Connect .p8 and the RSA signing key live in a secure Codemagic variable group, and the build creates or fetches the distribution certificate on the fly
  • Diagnosed the on device Firebase failure as an export packaging problem rather than a code problem, then fixed it by moving the credentials to firebase.env and adding *.env to the non resource export filters on the iOS and Android presets, restoring cloud save on both platforms
  • Wrote LivesManager, a global lives economy: a life spent per death, a rewarded ad refill at zero, and a wall clock regeneration deadline stored in the save so it keeps counting while the app is closed
  • Made regeneration survive a tampered device clock by never letting the remaining wait exceed one full interval, and ran its poll timer with PROCESS_MODE_ALWAYS so the countdown stays live while the out of lives popup pauses the tree
  • Drove lives maximum, enabled state and refill hours from Firestore through GameConfig, so the client retunes the economy from the console instead of cutting a new build
  • Reused the existing reward popup for the out of lives flow behind a mode flag, so a single popup and a single rewarded ad callback serve both the power up grant and the lives refill
  • Added first run touch onboarding: the floating joystick is invisible until touched, so it now appears once at a fixed lower left spot as a hint and hides for good after the first drag, with the flag persisted in the save
  • Fixed a dash that silently did nothing when pressed while standing still, by opening a short input window and dashing in the facing direction if no direction arrives
  • Integrated the AppLovin MAX iOS plugin (.gdip descriptor plus xcframework) and shipped the accompanying store and ads fixes alongside the Android Google Play Billing setup
  • Added the lives HUD on the player and the refill countdown on the main menu

Gallery

Straight after the rewarded ad: back to six lives, the counter restored and the refill deadline cleared. The touch HUD around the edges is the mobile layout the same build ships to iOS and Android.
Straight after the rewarded ad: back to six lives, the counter restored and the refill deadline cleared. The touch HUD around the edges is the mobile layout the same build ships to iOS and Android.
The main menu carries the same countdown, so a player who closes the app mid run can see when they will be able to play again. The two store entries sit bottom right.
The main menu carries the same countdown, so a player who closes the app mid run can see when they will be able to play again. The two store entries sit bottom right.
The same popup asking for an ad in exchange for a hint. One popup and one rewarded ad callback serve every offer in the game, told apart by a mode flag.
The same popup asking for an ad in exchange for a hint. One popup and one rewarded ad callback serve every offer in the game, told apart by a mode flag.
A third use of that popup: a rewarded ad for a slowdown power up.
A third use of that popup: a rewarded ad for a slowdown power up.
The slowdown running after the ad completes, with its own duration bar at the top and the world tinted for as long as it lasts.
The slowdown running after the ad completes, with its own duration bar at the top and the world tinted for as long as it lasts.

Breakdown

The refill deadline is a unix timestamp in the save file, not a running timer, so closing the app does not pause it. On launch and on every application resume the manager compares the deadline against the system clock and tops the player back up if it has passed. A deadline further away than one full interval is treated as a clock that has been wound back and is rewritten, so moving the device clock cannot park the player on a permanent wait.

The game already had a rewarded ad popup for power ups, so the out of lives offer became a mode on that popup rather than a second one. The Yes and No handlers branch once on the mode and hand off, which keeps a single ad callback, a single pause path, and a single place to change if the ad network is ever swapped.

Firebase looked broken and was not. It signed in, read the client's save out of Firestore and wrote one back, all from the editor against their live project. Only the built app failed, and it said why: "Unable to read Firebase config". The addon keeps its credentials in a plain .env file, and Godot's export step copies only files it recognises as game resources. The file was in the project and never in the build. That distinction was worth making carefully, because the visible symptom, progress resetting on the phone, reads like lost work rather than a missing setting.

Lives, ads and purchases are the kind of thing a client wants to move after launch, so none of the numbers are compiled in. GameConfig reads them from Firestore at startup, with the code falling back to sane defaults (six lives, a 24 hour refill) when the document is missing, so a bad or absent config degrades to a playable game instead of a broken one.

What the client said

5 out of 5
Great freelancer. Solved our Firebase project needs and IAP/ads needs relatively easily. Would work with him again.
boronatic, United Kingdom · 3 weeks

Credits

Release pipeline and systems programming
Bilal AhmadThe iOS build pipeline, lives system, touch onboarding, and the Firebase, ads and in app purchase fixes described on this page.
Desvelado, the game itself
The client studioThe game, its art, audio, writing and level design are the client's, and were already released on Steam and Android before I joined. My work sits on top of that build.
Third party integrations
godot-firebase, AppLovin MAX Godot plugin, GodotGooglePlayBilling, GodotPlayGameServicesExisting addons wired into the project and shipped, not written by me.

Built with

  • Godot 4.6.3
  • GDScript
  • Codemagic CI
  • App Store Connect API
  • TestFlight
  • Firebase Auth
  • Cloud Firestore
  • AppLovin MAX
  • Google Play Billing
  • Xcode
  • CocoaPods