Building standalone Expo mobile applications

Why do you need Expo and standalone Expo build?

Expo is an environment that contains services and tools to develop mobile applications written in React Native. It brings us a super useful ecosystem, which covers creating, building, publishing and updating RN apps right from CLI. This solution is blazing fast and after a few minutes, we can have complete builds, ready to run on our devices.

I think the most recognizable tool in Expo’s ecosystem is Expo mobile client, which allows us to test our application during development (with live reloading) only by scanning QR code from our terminal. This brilliant solution speeds up our work and makes it way easier!

But Expo doesn’t support only development, it helps us also in building and publishing process, by preparing packages for Android and iOS. We have to only upload our sources to Expo cloud and after a few minutes, our application is ready to download. And this is the moment when you want to consider standalone Expo build.

What do I mean by standalone Expo build? It’s a self-hosted application, which uses Expo tools and libraries without Expo’s cloud services for building, publishing, and updating. You may ask: why do I want to resign from this comfort and fast solution and build the same on my servers? There could be several answers to this:

  • You want to protect your sources and don’t push them to external servers
  • You don’t want to use AWS cloud (where Expo runs building tools and stores minified source codes and application assets)
  • Your client forces you to keep each step of your development process on his servers
  • …(put your own reason)

Expo is great and useful. We don’t want to resign from it just because of the above. Let’s create our own services to build and publish a mobile application.

Let’s check also, how to build standalone Expo mobile apps with Github Actions!

Generating keys for Android and iOS

Each mobile application has to be signed by specific credentials and keys. Since we decided to don’t use Expo cloud to publish our app (which means also building and updating), we have to take care of generating keys as well. Fortunately, it’s not a big problem if you have access to a terminal (for Android builds) and macOS (for iOS builds).

iOS credentials

Apple prepared for us a long path to generate required certificates. We have to start on the Certificates List, which is available from our developer’s account:

  • First of all, we have to create a new Identifier. The identifier contains basic information about our application, like desired platforms, App ID or (which is very important) Bundle ID. At the beginning we pick App IDs to register and then, we fill a simple form with all data mentioned above. We also can choose additional capabilities for our app, for example: push notifications or sign in with Apple service, etc..
  • Next, we need to create a certificate for our application. It’s a simple iOS Distribution certificate, which is needed by the next steps. In this place, you can also generate certificates for services (like push notifications) or for other channels of distributions. To generate a certification file, we have to introduce ourselves by CSR (Certificate Signing Request) file, generated from our Apple account on some macOS (more details, how to generate such file you can find here:
  • Based on just created Identifier and cert we have to generate Provisioning Profile and decide on which development/distribution channels we want to deploy our build.

If you plan to use push notifications in your application, it’s a good moment to generate also APN (Apple Push Notification) key. You can find it in Keys section. To generate .p8 file, you have to just provide some key name and select the APN option from the list.

The last step is to generate .p12 file, which is needed by Expo building tool. We can export such a file from Keychain Access Manager on our macOS, but before that, we need to install the certificate which we created at the very beginning (step 2).

After all these steps, we should have a complete list of required files, which means:

  • *.mobileprovision
  • *.p12 (with password of your choice)
  • *.p8 (for notifications)

Android keystore

For Android, on the opposite, we need one file, which we can generate by one simple CLI command. In your terminal just run keytool script and answer several questions about the certificate’s owner. This is the “magic” command:

$ keytool -genkeypair -v -keystore {filename}.keystore -alias {keystore-alias} -keyalg RSA -keysize 2048 -validity 10000

After that, we have our password-protected keystore file.

Building iOS and Android apps

When we have all keys and certificates, it’s time to build our packages. But before we start, it’s important to know, how Expo application works (and why they allow us to update them over-the-air).

When we open our application, Expo container asks for application manifest (which contains all metadata and paths to application sources) and then it downloads the latest sources from CDN servers (in most cases it’s cloudfront) according to the platform. This is also how Over-The-Air updates work: when sources are updated and we configure our app to ask for new sources on each launch, then we can update our app just by updating sources on CDN, without pushing a new build to store.

Export assets

As you may guess, if we decided to resign from Expo’s cloud support, we have to also host our sources somewhere. Actually, for Expo it’s no matter where our files will land. There is only one requirement: connection has to be secured by SSL.

Let’s assume that we have some hosting with SSL and we want to put there our sources. To do that, we need to go through some steps (they can be run on any platform - macOS, Windows or Linux)

  • Log in to your account by $ expo login - it’s a very important step if you want to have notifications in your app because manifest has to be signed by your account, on which you will store .p8 key (more details later in this article).
  • Remove ./dist folder if exists
  • Run export by $ expo export —public-url - as you can see, you have to pass the final URL, where your assets land.

After a few minutes, all your sources and images will be stored in ./dist folder and all these content you have to upload directly to

This set of assets contains manifests, images and minified bundles of our source code. It’s important to export and publish it before we start building final packages.


Expo team prepared a great tool called Turtle-CLI (, which handles the building process on their side, but they turned out cool guys and they published sources on GitHub to use it standalone. Thanks to that, we can include building mobile applications on our servers in the continuous delivery process. All we need is bash, node (8 or higher) and JDK8 - for Android, or macOS and Xcode (<= 9.4.1) - for iOS build.

Of course, to build anything, you have to install globally Turtle-CLI:

$ npm install -g turtle-cli


Since Android build doesn’t need many things to run, you can just start building it on your computer or use one of the docker images, prepared for such a task. For example, we have our own image, with all prerequisites and downloaded Turtle-CLI, prepared as a Jenkins slave. You can find it on DockerHub: (tag: expo-android).

To run build, you have to define the environment variable in your system:

  • EXPO_ANDROID_KEYSTORE_PASSWORD - a password for the keystore which we generated in the first part
  • EXPO_ANDROID_KEY_PASSWORD - a password for the key

And that’s it, time to run our build by:

$ turtle build:android -o {filename} --keystore-path {path-to-keystore}.keystore --keystore-alias {keystore-alias} --public-url {public-url}

Let’s explain it a bit:

  • filename - output filename of our build, it’s important to use a proper extension, like .aab for app-bundle or .apk if we decide to use the flag -t apk and build only APK package
  • path-to-keystore - relative path to key, which we generated by keytool
  • keystore-alias - alias, defined in keytool
  • public-url - URL to our assets, precisely to android manifest, in our example it would be:

And after a few minutes, we should have our application ready to publish.


The building process for iOS is quite similar. Of course, all these things we have to run on macOS. First of all, we have to set an env variable with a password to .p12

  • EXPO_IOS_DIST_P12_PASSWORD - a password to generated .p12 file

After that we can run turtle’s command:

$ turtle build:ios -o {filename} --team-id {apple-team-id} --dist-p12-path {path-to-p12} --provisioning-profile-path {path-to-pp} --public-url {public-url}

Small explanation:

  • filename - path to the output file with extension .ipa
  • apple-team-id - your Team ID, you can find it on your developer account
  • path-to-p12 - path to generated .p12 certificate
  • path-to-pp - path to the generated provisioning profile
  • public-url - URL to our assets, precisely to ios manifest, in our example it would be:

This build will also take a few minutes and after that, you should be able to find IPA package in your workspace, ready to publish.

Notifications in your standalone applications

Mobile application without push notifications nowadays sounds a bit oddly. Our phone vibrates almost all the time. If you want to add such a feature to your standalone Expo app, you have to accept some compromise and store some credentials on Expo servers.

Expo allows us to use FCM directly as a notification broadcaster with one restriction: this solution doesn’t work on iOS. So, probably such an option won’t satisfy you. Me neither. Let’s configure Expo-based solution then.

Expo has its own broadcast service, which propagates our notifications through FCM straight to our applications, so we have to start from proper FCM configuration.

FCM configuration

On Firebase Console (, we have to create a new project, with two applications - Android and iOS. Android app configuration is quite simple: everything we need to do is to fill out a simple form by app name/bundle ID and to download google-service.json file at the end of this process.

iOS configuration is a bit more complicated because we have to upload our .p8 file, generated in the Apple Developer panel and provide .p8 ID and Team ID.

After configurations, we should have one extra file (google-services.json), which we save right into our project’s root directory. We have to also add a path to this file in our app.json file:

"expo": {

  "android": {


    "googleServicesFile": "./google-services.json"



From now, we have to remember to add google-services.json file to our project’s root directory for each export and build step. If we forget about it, none of these steps will pass.

Register keys on Expo

Let’s go back to our terminal and push some information to Expo servers. First, we have to send FCM server key, which we can find in Firebase Console on Cloud Messaging tab. To do this, we use Expo CLI:

$ expo login
$ expo push:android:upload --api-key ${your_server_key}

And that’s everything. For Android, of course. Keep in mind that you have to be logged in before you start registering any keys.

iOS case is a bit more complicated. Since Turtle-CLI doesn’t support uploading .p8 files yet, we have to use a small, dirty workaround to cheat expo and send this authorization key to its servers somehow. We need one environment variable, which we already defined. Let’s change it to some random phrase (to avoid sending real passwords to expo servers), it could be abc or anything else.


Next, create an empty file in your project dir and call it fake.crt and then make some magic in your terminal by running:

$ expo login
$ expo build:ios --push-id ${APN_ID} --push-p8-path ${path_to_p8_file} --apple-id ${your_apple_id} --dist-p12-path ./fake.cert --provisioning-profile-path ./fake.cert --no-publish

Some explanations:

  • APN_ID - is an ID from your APN key, in most cases, it’s a part of p8 filename
  • pathtop8_file - relative path to your .p8 file
  • apple_id - Apple ID, which is necessary to start uploading

Keep in mind, that you need properly filled app.json, with proper bundle IDs and app names. This command will start building iOS application on expo servers, but before that, you will be asked to answer two questions:

  • Password for your Apple ID account. The answer to that is quite obvious, I hope.
  • How would you like to upload your credentials? And here we have to choose the option: I will provide all the credentials and files needed, Expo does limited validation

After the last confirmation, we have to wait for a second, until we get a URL to build preview, and then kill the process by CTRL+C. Just to be sure, you can check this URL and cancel the build, but probably it will be already finished because of our fake certs.

It sounds super-strange, I know, but for now, this is the only way to upload p8 file to Expo servers.

To make sure that our credentials are in the right place, we can check it by Expo CLI command:

$ expo fetch:ios:certs

The response should include our fake P12 password and proper Push Key ID.

After all these steps, we can proceed to handle notifications in our codebase according to the official documentation:


As you can see, Expo is not only a useful library, which supports our development but also they provide a bunch of tools to build our applications on our own. Anyway, it sounds a bit overcomplicated and probably you won’t need such workarounds to build your app, but it’s good to know that there is such an option. You can migrate your codebase and processes to your servers any time. And it’s no one-way ticket, you can always decide to use Expo resources to publish your application again, but keep in mind, you have to upload all credentials, generated for a standalone app, to Expo servers, to keep the continuity of your application.

The whole process could be run on Github Actions. Check here: how to configure your own, github-based CI/CD for mobile apps.