Building On-call: Building a multi-platform on-call mobile app

A significant part of being on-call is the ability to respond to pages and handle escalations on the go. In the early stages of developing incident.io On-call, we considered whether a Minimum Viable Product (MVP) could rely solely on SMS and phone calls. However, we quickly realized that a fully featured mobile app was going to be essential to the on-call experience.
This led us to the question: how should we build this mobile app?
Choosing a technology
Initially, we drew from prior experience and spoke to peers elsewhere about their experiences building and maintaining mobile apps. The main question was what technology should we be using?
Native iOS and Android
If you ask Apple or Google, a native app built in Swift or Kotlin is the way to go (ignoring Googleâs Flutter for a moment). With a native app, youâll work directly with each companyâs SDKs, making it the easiest option to build OS-specific features and adhere to UI and UX standards.
However, youâre writing code specifically for that platform and thus will be building and maintaining two distinctly separate apps.
Kotlin Multiplatform
Aiming to bridge the gap around writing two separate apps is Kotlin Multiplatform. This is a technology that enables you to write your code once in Kotlin and run it on both iOS and Android. Often, this is done such that you write shared logic in Kotlin, but write your UI elements for each platform separately in their own styles.
This is a relatively new technology, having been declared âstable and production readyâ in November 2023, which happened to be right around when we were trying to choose a technology ourselves.
Flutter
Flutter is another option from Google for building a mobile app. With it, you write your code in Dart, which gets rendered using Flutterâs own rendering engine on both iOS and Android.
As youâre running the same rendering engine on both platforms, Flutter makes it easy to have a consistent look and feel for your app across platforms, but this can be at the cost of your app feeling a little uncanny-valley to the OS its running on.
React Native
React Native is the final option we considered. With it, you write React code using Javascript. The UI that you render uses real native UI components under the hood, so the argument is that you can build real native apps across both platforms using Javascript.
However, not everything you do in a Native app is readily available in React Native, with some core things like navigation being left to you as a user to work out.
To help guide our decision, we spoke to experts in each technology, but we brought our own set of preferences too. These included:
- We wanted an app that felt great to use. We believe this is possible with any mobile technology, but some might require more effort than others.
- We were not keen on maintaining two separate apps. If possible, we wanted to write code once and have it run on both iOS and Android.
- We wanted everyone on our team to be able to contribute to our app. Regardless of the technology we choose, there will likely be some learning curve, but we wanted to keep it as manageable as possible.
Ultimately, we landed on React Native as our main candidate for a technology. Weâd heard of Expo, described as the âVercel of React Nativeâ, and decided to prove out our plans with an upcoming hackathon.
Initial exploration
We took advantage of a planned company hackathon to spike out using React Native and Expo. We assembled a small team of two engineers and a designer, and set to work on trialling the developer experience and overall UX. The project we built leaned into Expo as much as possible. We began with Expo Go and their project template.
Expo
Expo Go is an app that anyone can download from the App/Play Store and acts as a mini execution environment for your Javascript code. It comes with a bunch of useful native dependencies built in, such as Expo Router and Reanimated.
Whatâs great with Expo Go is that you can get developing on an app without having to set up your machine with all the tools you normally need for app development.
Rather than installing Xcode, Android Studio, Java, Cocoapods, etc., you instead just download Expo Go to your phone, run yarn start to run a Javascript server and scan a QR code.
Instantly you have your app running on a physical device with legitimately good hot reloading. In our experience, you could go from nothing to making code changes, faster than it can take to download Xcode.
At no point with an Expo Go set up do you need to compile and run any native code yourself. To demonstrate that, hereâs a simplified version of the structure of our mobile app repository (no build.gradle or IncidentIO.xcworkspace to be seen!).
âââ README.md
âââ app
â  âââ ... our app structure is defined here
âââ app.config.ts
âââ assets
âââ babel.config.js
âââ components
â  âââ ... our components are defined here
âââ eas.json
âââ index.js
âââ metro.config.js
âââ package.json
âââ tailwind.config.js
âââ tsconfig.jsonReplicating our web codebase
Our web dashboard is built using tools like React, React Router, SWR and Tailwind. We wanted to explore how similar we could make our React Native code, as to minimise context switching across the two surfaces.
In our hackathon, we took our bare Expo template project and started adding some dependencies to make things feel a little more like home:
- We opted the generated mobile app into our Yarn Workspace. This meant we had an autogenerated, type-safe API client ready for usage. We also installed a Vercel library we use elsewhere called swr, which we use alongside our autogenerated API client to make fetching data really easy. For example, I can get a list of schedules as simply as:
const { data: schedulesResp } = useAPI("schedulesList")- We added Nativewind, a React Native port of Tailwind CSS, which gave us a familiar environment for styling components. On top of this, we made our tailwind.config.js use semantic colours via CSS variables, meaning that we support light and dark mode out of the box.

- We also installed Expo Router, which gave us directory structure based routing, like we have on our Next.js application that powers our Status Pages product. In doing this, we get out of the box support for things like deeplinking from push notifications, and a handy âsitemapâ available from Expoâs dev menu.
Transitioning to a development build
A key part of the on-call app is receiving push notifications. This isnât something you can do with Expo Go, as doing so relies on you generating a push certificate in your own Apple Developer account, which is tied to your appâs bundle ID, rather than Expoâs generic wrapper app.
Despite this, we still wanted engineers to be able to contribute to the mobile app without needing to set up a native app environment and compile it themselves locally.
Fortunately, Expo have a solution built with this in mind. You can utilise what they call a âmanagedâ workflow and their âprebuildâ solution to ephemerally generate ios and android projects specifically for your project. Rather than maintaining an Xcode project or a build.gradle, you instead have an app.config.json file where you can define all the different config about your app. This includes things like:
- The name of your app
- The bundle ID of your app
- The app icons you want your app to use
- Any
Info.plistorAndroidManifest.xmlattributes that youâd like to set
To get push notifications working, we started defining our config file and then utilised Expoâs Application Services (CI tooling) to build the native components of our app on their side. Once thatâs done, we get a little QR code that we can scan to install our own little wrapper app. It looks a lot like Expo Go, but has our own packages installed and is configured for our push certificates.
Result of the hackathon
Once weâd got our dependencies set up and running, iterating on React Native was a breeze. With two of us working for two days, we built a basic app that could login, show if youâre on-call, and send you push notifications when you were paged.

In reviewing our goals for a mobile app technology, we really thought React Native ticked a lot of boxes:

- We built an app that felt native. We really cared that the native gestures that people are used to, work just like theyâre used to on our app. Thereâre no uncanny-valley gestures that almost feel right but something is just off.
- We also built both an Android and iOS app from a single codebase. There were quirks from time to time that we had to solve for, where something would be working on one device, then youâd test it on another and realise performance was terrible, but nothing too egregious.
- Finally, we created a codebase that could easily be mistaken for our web codebase. The code is really quite similar. Thereâs perhaps even a future where we start sharing some code and components between the two - weâve already got this happening for our generated API package, it might not be crazy to apply it to building our UI too.
Productionizing
Building for a hackathon is one thing, but making something publicly available is a whole other kettle of fish, as Iâm sure some of you are well aware!
Following our hackathon, we returned to developing the rest of our On-call product: building out support for schedules, escalation paths and the executor that runs the show when you are page. Just over a month later, we had a basic end-to-end on-call product, and we felt it warranted a real mobile app in order for us to dogfood it properly.
Rather than start from scratch, we took the hackathon project and stripped it back to a bare minimum. This avoided us redoing work to get dependencies and whatnot in place, but cleared the decks so that we werenât tied to partly-functional hackathon implementations.
The core features we focussed on initially were logging in, setting up notifications and acknowledging pages. Iterating on these was really quite a smooth experience and we made quick work of them. The areas we had less success in were more related to CI/CD tooling and also working with some mobile APIs to work with your system volume and mute switches.
Build tooling
For our Go backend and React web dashboard, we use CircleCI as our CI/CD build tool of choice. When it came to mobile however, Expo offer a build tool that we wanted to make use of. Given building React Native apps is their area of expertise, we wanted to offload that work to them. This gave us benefits like:
- Not having to deal with certificate signing, distribution certificates and other ârare to deal with, but real time sinks when you need toâ things for mobile development. Expo manage most of these for you.
- Easy management of build environments. Had we used CircleCI, weâd need to set up separate Android and iOS build images and manually write some of the build pipelines that you get for free with Expo.
- Things like QR codes that you can scan to download builds are another thing that you get out of the box on Expo.
As we mentioned above though, this wasnât all smooth sailing. When I first tried building with Expoâs build services, everything went fairly smoothly. But later, as I adopted our autogenerated API client, by opting the mobile app into our Yarn workspace, things started to go awry. I started to get build errors when pushing to Expo that I couldnât reproduce at all locally.
Speaking of locally, Expo donât really give you the tools to replicate their build environments locally. Itâs up to you to match the version of Xcode, Ruby, Cocoapods, etc. that theyâre using. Most people working on our React Native app will never need to think about this sort of stuff, but when you are working on it, it can feel a bit like youâre working at the operating table wearing a thick pair of gloves - everything is a little abstracted and distant.
To fix our Yarn workspace issues, we utilised some CLI tools that Expo provide to try and run CI builds locally. What we found was that the source code directory being uploaded to Expo contained some files we definitely didnât need to upload (such as our Go source code), and was missing some files we definitely did need to build a valid Javascript bundle. By creating an easignore file, we could specify exactly which files were needed.
Platform specific APIs
For most things you need to do in React Native, you can often find a package where someoneâs implemented the native <> Javascript wrapper that you need. Expo themselves make and maintain a number of these packages, and there are thousands of other packages for just about everything you can think of.
For our app, we almost got away without having to write any Swift or Kotlin ourselves, until we got to a critical feature of on-call â having your phone ring (loudly) when you are paged.
On iOS, making your phone ring, even if your phone is in do-not-disturb or in silent, is an operating system supported thing. To do so, you simply request permission to send âcriticalâ alerts, and then when sending push notifications to APNS from your backend, you set a boolean flag on the notification payload that says { "critical": true }.
For Android however, thereâs no built-in way to achieve such a thing. Given this is quite a niche use-case, there were also no React Native packages we could install that would do such a thing. This meant we had to roll up our sleeves and get writing some Kotlin.
If youâre a mobile or React Native developer, that might have you thinking that we had to concede our development set up where you only needed to run the Javascript, and you never need to build any native code. But, thereâs an Expo feature called âmodulesâ made for exactly this use case. It allows you to define bits of native code as âpluginsâ for your ephemerally generated projects. This means that when we build our app on Expoâs side, theyâll generate an Android project from our config file, and then apply a plugin weâve written that contains all of our native Kotlin code and install it as a Gradle dependency on the generated project.
This is a fairly new feature to Expo and we found the docs a little confusing, it required a reasonable bit of trial an error. We also werenât great Kotlin engineers. I personally had some experience writing Kotlin itself, but much of the process involved working out things like Gradle dependencies, which Iâd done much less of.
Nonetheless, the feature worked just as weâd hoped and we managed to build our own tiny Kotlin package that has responsibility for making your Android phone ring, even if itâs on silent.
Conclusion
At the beginning of March this year we shipped our On-call with a mobile app. We actually wrapped work on the âMVPâ app in early February, as to give ourselves time for app store review processes (which was fortunate as we found this was much needed!).
In general, weâre happy with the stack weâve chosen. Iterating on features is quick, engineers can onboard with code that looks familiar to them, we can remotely push Javascript updates if we really break things.
Additionally, weâve built an app that feels great to use. Thatâs not something weâve covered in detail here, I think Iâll save that for another blog post, but I feel that any mobile technology can build great apps if you put in the effort to make it so.
There are definitely still thorny parts to React Native - tasks that seem menial like upgrading a library can turn into a multi-day battle with CI tooling, but itâs far more stable than Iâd previously experienced when I tried React Native five years ago.
I hope this has provided some useful insight into what it takes to build a greenfield mobile app in 2024. Weâre actively adding new features to On-call all the time, and our mobile app is going to be a huge part of that.

More from Behind-the-scenes building On-call

Behind the scenes: Launching On-call
We like to ship it then shout about it, all the time. Building On-call was different.


Building On-call: Our observability strategy
Our customers count on us to sound the alarm when their systems go sidewaysâso keeping our on-call service up and running isnât just important; itâs non-negotiable. To nail the reliability our customers need, we lean on some serious observability (or as the cool kids say, o11y) to keep things running smoothly.


Building On-call: The complexity of phone networks
Making a phone call is easy...right? It's time to re-examine the things you thought were true about phone calls and SMS.

So good, youâll break things on purpose
Ready for modern incident management? Book a call with one of our experts today.

Weâd love to talk to you about
- All-in-one incident management
- Our unmatched speed of deployment
- Why weâre loved by users and easily adopted
- How we work for the whole organization



