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?
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?
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.
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 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 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:
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.
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 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.json
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:
const { data: schedulesResp } = useAPI("schedulesList")
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:
Info.plist
or AndroidManifest.xml
attributes that you’d like to setTo 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.
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:
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.
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:
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.
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.
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.
We like to ship it then shout about it, all the time. Building On-call was different.
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.
Making a phone call is easy...right? It's time to re-examine the things you thought were true about phone calls and SMS.
Ready for modern incident management? Book a call with one our of our experts today.