It’s the 2nd week of Shipaton and I thought it was a good time to take a moment to document my progress on the submission.
Note: This is the 2nd post about my work towards shipping an app for the Shipaton 2026 hackathon. If you want to read from the beginning you can find the link to the first post here
As a recap I’m building a multi-layered application using the following technologies:
- Kotlin Multiplatform (Shared Kotlin Library, handles networking, file storage and business logic)
- iOS App (iOS app built using SwiftUI, focuses on the UI layer and consumes the Kotlin Multiplatform library for its business logic)
- Firebase Backend (An all-in-one backend using Firebase Cloud Functions for endpoint creation / webhook event handling, Firebase Cloud Messaging for push messaging and Firestore for backend persistence)
Let’s start with what I have. I have a working app, complete with an onboarding flow that guides you through linking your GitHub account to the Buildhorn GitHub app (more on that later!). Once you’ve given permission to access your selected code repositories, Buildhorn can monitor workflow events for those repositories via a webhook and push the information to the app.
Once you are through to the home screen you are shown a list of your monitored repositories where you get the latest build status. You can also create iOS widgets to show the status for each repository outside of the app.
The monitoring is done via the Firebase backend, where Cloud Functions handle much of the GitHub authentication and synchronisation. Information about what repositories to monitor and the run status for each are stored in Firestore.
The free version limits users to connecting two repositories. I still need to hook up RevenueCat to enable users to connect unlimited repos. It’s not a pretty app but it works, and the polish will come in the next few weeks.
Here’s a video of how it looks in action:
Let’s dive deeper and look at how each part of the application is getting on.
Kotlin Multiplatform
The first thing I wanted to do was create a stable foundation for the project. That meant setting up the Kotlin Multiplatform library and configuring it correctly to make sure communication between KMP and iOS was seamless and issue-free.
To start, I installed the KMP Plugin for Android Studio and used the Kotlin Multiplatform template from the new project wizard to get my project set up.
The template does a good job of setting you up with everything you need. It creates the KMP shared library, as well as the native apps for iOS and Android that are preconfigured to pull in the shared library. You can even run an iOS app straight from Android Studio and use the debugging tools to debug your KMP library, very handy!
One part of the starter project I had to remove was the built-in wiring for Compose Multiplatform. I want my project to leverage the best practices of each native UI framework, which meant Compose Multiplatform unfortunately didn’t have a place here.
Fortunately, a few prompts to Claude made removing Compose Multiplatform an easy job, keeping the iOS UI using SwiftUI and updating the Android app to use the Jetpack Compose libraries, in case I want to build an Android app later.
Next on the list was to find suitable dependencies for the library. I want KMP to own as much as possible in the data and domain layer, that means things like local data storage, dependency injection, view model management, Firebase interaction, and of course RevenueCat subscription management.
To figure out what was available, I used klibs, a website dedicated to cataloging KMP libraries. After some browsing, I ended up using:
With that I had my tools to begin working on the business logic. For the architecture I’ve opted for a traditional Android api / impl architecture to lean into my existing Android / Kotlin skills.
iOS App
With the KMP starter project setting up the iOS App, all I had to do was begin working on the SwiftUI screens and hook up the viewModels exposed via the shared library. I started off with the onboarding screens where I began to run into a few interesting questions.
First, how do I link my user’s GitHub installation to my app, and what do I do if I want to send them push messages? I decided to keep things as light as possible and use Firebase’s anonymous authentication so at least I have an ID to use that’s linked to a user’s GitHub installation. That way I can start generating push tokens for later, it also streamlines onboarding.
Another issue that arose was making sure calling through to the Kotlin library felt idiomatic in Swift. There are translation issues, particularly when you start using Kotlin’s suspend APIs alongside Swift’s concurrency model, or exposing Kotlin’s Flow APIs to Swift.
Fortunately, the KMP community has provided two libraries to solve this, SKIE and KMP-NativeCoroutines. These libraries help make your Kotlin code produce more Swift-friendly APIs so Swift can use your library seamlessly.
Thanks to these I was able to get to a point where I can even call out to the KMP shared library and get information into iOS widgets:
Firebase Backend
To create the backend interfacing between the app and GitHub, I created a separate code repository containing the Cloud Functions. Cloud Functions are essentially a repackaged version of Google Cloud’s Cloud Run Functions. Like other serverless frameworks, most of the hardware configuration is abstracted away, meaning you can focus on implementing the backend logic you need.
I also need backend persistence, which comes in the form of Firestore. It’s a NoSQL database that follows the document data model pattern. So far I have three collections:
- Installations (Stores every installation of the Buildhorn GitHub App)
- Users (Tracks the FCM token / installationId)
- runStatus (Tracks every workflow event received from GitHub for authorised repositories)
I needed a few functions to interact between GitHub, Firestore, and the iOS app. These are:
- Webhook (Handles the events received from the GitHub webhook, authenticates the webhook’s HMAC signature for security)
- Installation (Handles installation events sent from GitHub)
- Authentication (Authenticates the JWT returned from GitHub to validate the installation)
- List Repos (Lists the repos the user has granted Buildhorn access to, so they can choose which to monitor)
- Update Selected Repos (Saves the user’s chosen repos to Firestore once they’re selected during onboarding)
- Run Status (Writes the workflow status to Firestore when a webhook containing a workflow event comes in)
- Get Tracked Repo Status (Returns the workflow status for each repository the user has given Buildhorn access to)
One thing I’m quite happy with is how the Run Status function copes with GitHub redelivering the same webhook event, which happens often. Rather than keeping a separate table to track which deliveries I’ve already seen, the function runs as a Firestore transaction that compares the incoming result against whatever is already stored.
If it’s a redelivered webhook event, it reads back what it already wrote and carries on. This is also handy from a UX perspective, as a repo that’s already showing as failing won’t spam you with a fresh notification every time GitHub resends the same event. You only get pinged the moment it actually flips into a failing state.
This code is extremely scrappy and I’m pretty sure the Firestore collections can be cleaned up. It does the job for now, although there is room for tidying up here once the app is stable.
The last part of the puzzle here is making sure there is a GitHub App available to deliver the events. GitHub encourages integrations to register their own App on the platform, which means users have to authorise the App to access their repos as part of onboarding.
Fortunately, the process is straightforward and heavily documented. Following that I was able to create and configure a GitHub App, so now there’s a Buildhorn GitHub App.
This makes the onboarding flow somewhat tricky as the user needs to go to GitHub, install the app and select what repositories to give access to. Then the onboarding needs to ask what repos to monitor. This is important because the free version only supports monitoring two repositories.
Any more than that and the user needs to subscribe, we’re trying to make some money here! :)
AI Tools & Skills
It’s fair to say I wouldn’t be able to make this much progress for Shipaton without the use of AI tools. The ability to explain what you need in a few sentences and see the code generated before your eyes is incredibly helpful, if still somewhat unnerving having spent most of my career writing by hand.
I’m definitely in the realm of vibe coding here instead of following an Agentic AI workflow. For a project like this, that feels okay. If I want to standardise the development later on, I could look at ways of doing that.
I’m using Claude Code, Claude Design, and Junie as my tools of choice. I pay for a Claude Pro subscription, so it’s the one I reach for first.
Claude is working quite well. I use it as my “project manager” and “designer” via Claude Design, and as my “coder” across Android Studio for KMP/iOS and Visual Studio Code for the Cloud Functions.
It’s responsible for the high-level plan of the project: what steps I should be doing, and in what order. I also ask it to break down the steps into substeps, so I know what part of the project to work on next. The result is essentially a .pdf file I regularly refer back to, often iterating on when needed.
The issue I’ve spotted is that once I deviate from the plan before letting it know, it’s difficult to get Claude to update the plan to reflect what’s actually changed. This is even if I ask Claude in another session to provide an update that I can feed back into the plan. Claude seems to be stubborn in sticking to the original plan, instead of freely adapting to the change.
I’ve had relative success with Claude Design for getting these MVP screens generated. I feel the true test for it will be when I want to polish the experience, that might make for a separate write-up in the next few weeks.
Junie is something I’ve been given free access to as part of JetBrains’ participation in Shipaton. I am finding I usually jump to it when my Claude usage is exhausted and so far I’m impressed. I use it inside Android Studio. The main thing I like about it is its conciseness. It doesn’t seem prone to being verbose or overly wordy.
It also seems to have a distinct advantage over Claude for its KMP knowledge, although that’s more of a feeling than anything I have concrete data on. I would hope it is, considering KMP is also developed by JetBrains. I envision I’ll find myself relying on it more for the KMP parts of the project, I’ll also experiment with how well it handles Swift.
Since I’m trying to have something ready in an MVP form, I find myself focusing more on the outcome rather than the code quality at the moment. Claude’s and Junie’s output is pretty good, but I do like to make sure there are guardrails in place. So far these are coming in the form of:
- Firebase Agent Skills
- A custom CLAUDE.md / AGENTS.md (Junie uses it) to explain the main purpose and architecture of the App & Backend, and common do’s and don’ts.
It’s a barebones setup but I’m finding it’s enough to help guide Claude & Junie for the tasks I give it.
Next Steps
I feel I’m making good progress after two weeks. I am keen to have something ready and published to the App Store by the end of the month. The main tasks to get there are:
- Adding RevenueCat support for subscriptions
- Polishing the app
- Setting up a TestFlight external group for people to test the app and get feedback
- Thinking about the marketing strategy
That’s all for this post. Thank you for reading and look out for the next post on how progress goes building for Shipaton 2026!


Top comments (0)