Code signing verification process
When the app downloads an update, the server responds with the update and its generated signature. After being downloaded but before being applied, the update is verified against the embedded certificate and included signature. The update is applied if the certificate and signature are valid, and rejected otherwise.
Key rotation definition and purpose
Key rotation is the process by which the key pair used for signing updates is changed. It is performed in response to key expiration (after the certificate validity duration), private key compromise (if the private key is exposed), or as a security best practice to periodically rotate keys to ensure system resilience.
Key expiration and update application
After a certificate expires, updates signed with the corresponding private key will no longer be applied after being downloaded by the app. However, updates downloaded before the expiration of their signing certificate will continue to function normally. This means users must receive a new build with a new certificate before the old certificate expires.
Key rotation procedure
To rotate keys: (1) Back up the old keys and certificate. (2) Generate a new key by running the generation command from step 1, optionally changing the keyid in updates.codeSigningMetadata.keyid in app.json for debugging. (3) Set a new runtime version for builds using the new certificate to ensure only updates signed with the new key run in the new build. (4) Publish signed updates using the new key with the eas update command.
Removing code signing from an app
Removing code signing can be thought of as a key rotation to a null key. The procedure is: (1) Backup the old key and certificate. (2) Remove the updates.codeSigningMetadata field from app.json. (3) Set a new runtime version for builds to ensure that only unsigned updates run in the new build.
EAS Update code signing availability
EAS Update Code Signing is only available to accounts subscribed to the EAS Production or Enterprise plans.
Code signing mechanism in EAS Update
The expo-updates library supports end-to-end code signing using public-key cryptography. Developers can cryptographically sign updates with their own keys, and signatures are verified on the client before the update is applied. This ensures that ISPs, CDNs, cloud providers, and even EAS itself cannot tamper with updates.
Generate code signing key pair command
Run `npx expo-updates codesigning:generate --key-output-directory ../keys --certificate-output-directory certs --certificate-validity-duration-years 10 --certificate-common-name "Your Organization Name"` to generate a key pair and code signing certificate. The command creates three files: ../keys/private-key.pem (private key), ../keys/public-key.pem (public key), and certs/certificate.pem (code signing certificate valid for the specified duration).
Private key storage for code signing
The generated private key must be kept private and secure. It is recommended to store the private key outside of source control in a directory separate from the project, and to store it using the same methods as other sensitive information (KMS, password manager, etc.).
Public key and certificate storage for code signing
The public key may be stored alongside the private key but is not sensitive. The certificate should be included in the project and checked into source control. It contains the public key and a method for verifying code signatures, and is used to verify the update's signature when a signed update is downloaded.
Certificate validity duration considerations
The certificate validity duration may vary based on the security needs of the app. A shorter validity duration requires key rotation more frequently and is considered better practice since a compromised private key will have sooner expiration limiting exposure, but adds overhead to the app's release process. Longer validity durations reduce rotation frequency but increase risk if a key is compromised. Expo uses 20 years for the public Expo Go app, 1 year for internal apps distributed more frequently, and plans to rotate keys every 10 years.
Configure code signing command
Run `npx expo-updates codesigning:configure --certificate-input-directory certs --key-input-directory ../keys` to configure code signing in your project.
Code signing configuration with CNG
If using Continuous Native Generation (CNG) to generate native projects, the app.json configuration generated by `npx expo-updates codesigning:configure` is all that is needed. The changes will be applied to the native projects the next time they are generated.
Code signing app.json configuration format
When code signing is configured, the app.json includes the following configuration under the "expo.updates" section: "codeSigningCertificate" (path to certificate file), and "codeSigningMetadata" object with "keyid" (string, typically "main") and "alg" (string, typically "rsa-v1_5-sha256").
Android code signing configuration without CNG
For projects not using CNG, code signing must be configured in android/app/src/main/AndroidManifest.xml by adding two meta-data fields to the <application> element: one named "expo.modules.updates.CODE_SIGNING_CERTIFICATE" with an XML-escaped certificate value, and one named "expo.modules.updates.CODE_SIGNING_METADATA" with value "{\"keyid\":\"main\",\"alg\":\"rsa-v1_5-sha256\"}". To create the XML-escaped certificate, copy contents of certificate.pem and replace all \r with 
 and \n with 
.
iOS code signing configuration without CNG
For projects not using CNG, code signing must be configured in ios/project-name/Supporting/Expo.plist by adding two keys to the <dict> element: EXUpdatesCodeSigningCertificate (string value containing XML-escaped certificate where \r is replaced with 
 but \n is not escaped), and EXUpdatesCodeSigningMetadata (dict with keys "keyid" (string "main") and "alg" (string "rsa-v1_5-sha256")).
Publish signed update command
Run `eas update --private-key-path ../keys/private-key.pem` to publish a signed update for your app. During the EAS Update publish, the CLI automatically detects code signing configuration, verifies the integrity of the update, creates a digital signature using the private key locally (so the private key never leaves the machine), and sends the signature to EAS to store alongside the update.
AppDelegate setup for EAS Update on iOS SDK 52
AppDelegate should: (1) extend EXAppDelegateWrapper, (2) provide a public static shared() method to get the AppDelegate instance, (3) maintain a var updatesController reference of type InternalAppControllerInterface, (4) override bundleUrl() to return updatesController?.launchAssetUrl() if available or a bundled URL, (5) in didFinishLaunchingWithOptions set moduleName='App', initialProps=[:], rootViewFactory=createRCTRootViewFactory(), and call AppController.initializeWithoutStarting().
Migration from CodePush to EAS Update
Developers migrating from CodePush to EAS Update should reference the Migrating from CodePush guide for additional information on the transition process.
What EAS Update does for your app after release
EAS Update enables over-the-air updates for native Android and iOS apps. After integrating it, the app can receive JavaScript bundle updates from the updates system without requiring an app store submission, using the managed AppController that starts during app launch and loads the latest update or embedded bundle.
EAS Update in brownfield native apps with Expo SDK 52+
EAS Update can be integrated into existing native Android and iOS apps that have React Native installed. This guide applies to Expo SDK 52 or later and React Native 0.76 or later. For greenfield React Native apps (built with React Native from the start with React Native as the entry point), use the Get started with EAS Update guide instead.
Prerequisites for EAS Update integration in native apps
Prerequisites are: (1) A brownfield native project with React Native installed and configured to render a root view, following React Native's Integration with Existing Apps guide. (2) Latest Expo SDK version and its supported React Native version. (3) No other update libraries (remove react-native-code-push or similar). (4) Expo modules installed and configured in the project. (5) metro.config.js must extend expo/metro-config. (6) babel.config.js must extend babel-preset-expo. (7) npx expo export -p android and npx expo export -p ios must run successfully.
Disable automatic updates initialization on Android
Modify android/gradle.properties to disable automatic updates initialization by setting a property that prevents expo-updates from automatically setting itself up in a way that only supports greenfield React Native projects.
Disable automatic updates initialization on iOS
Pass the environment variable EX_UPDATES_CUSTOM_INIT=1 when running pod-install to disable automatic updates initialization. Use the command: EX_UPDATES_CUSTOM_INIT=1 npx pod-install (npm), EX_UPDATES_CUSTOM_INIT=1 yarn dlx pod-install (yarn), EX_UPDATES_CUSTOM_INIT=1 pnpm dlx pod-install (pnpm), or EX_UPDATES_CUSTOM_INIT=1 bunx pod-install (bun).
Metro config for brownfield EAS Update
Ensure metro.config.js extends Expo's metro config using getDefaultConfig from expo/metro-config. Example: const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); module.exports = config;
Custom entry point with registerRootComponent for EAS Update
Use Expo's registerRootComponent() in custom entry points. This registers the component with the react-native AppRegistry and performs all required Expo initialization including expo-updates setup. Example: import App from './App'; import { registerRootComponent } from 'expo'; registerRootComponent(App);
Custom entry point with direct AppRegistry for EAS Update
If keeping an existing entry point using AppRegistry directly, add a call to Expo initialization before registering the app. Import 'expo/src/Expo.fx' before AppRegistry.registerComponent(). Example: import App from './App'; import 'expo/src/Expo.fx'; import { AppRegistry } from 'react-native'; function getApp() { return <App />; } AppRegistry.registerComponent('App', () => getApp());
MainApplication.kt setup for EAS Update on Android
MainApplication class should: (1) implement ReactApplication, (2) override reactHost to use ExpoReactHostFactory.getDefaultReactHost() with applicationContext and packageList parameters, (3) in onCreate() call loadReactNative(this) and ApplicationLifecycleDispatcher.onApplicationCreate(this), (4) override onConfigurationChanged to call ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig).
MainActivity.kt setup for EAS Update on Android
MainActivity class should: (1) subclass com.facebook.react.ReactActivity, (2) override getMainComponentName() to return the app name registered in the JS entry point, (3) override createReactActivityDelegate() using ReactActivityDelegateWrapper that wraps DefaultReactActivityDelegate, passing this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, and a DefaultReactActivityDelegate instance.
CustomViewController setup for EAS Update on iOS SDK 52
CustomViewController should: (1) implement AppControllerDelegate, (2) in init set appDelegate.updatesController to AppController.sharedInstance, set AppController.sharedInstance.delegate to self, and call AppController.sharedInstance.start(), (3) implement appController(_:didStartWithSuccess:) that calls createView(), (4) in createView() get the rootViewFactory from appDelegate.reactNativeFactory, create a root view with withModuleName matching appDelegate.moduleName, and add it to the view controller with layout constraints to safeAreaLayoutGuide edges.
Channel surfing for production preview with internal users
Teams can roll out an update in production to a small set of internal users using channel surfing for a known subset of users. This approach should only be used with users who can report and recover from preview update issues because a broken update can prevent them from reaching the UI that clears the channel override.
Development builds for previewing updates
Development builds are a great way to preview updates from pull requests, directly from the EAS dashboard, or from the built-in UI provided by the expo-dev-client library.
Preview builds for non-technical users
Non-technical users can test changes from a preview build on an app store testing track or internal distribution. For smaller teams, a single preview build can be deployed to an app store testing track or internal distribution, and updates can be published to the channel used by that preview build.
Channel surfing in preview builds
A mechanism can be built into a preview build that allows users to select a different update or channel to load at runtime. This is useful when the app runtime does not change often and many different updates can be loaded in the same app.
Testing updates in production-like environment before deployment
Before deploying an update to production, it is often necessary to test it in a production-like environment. There are different approaches available for previewing updates depending on whether you are using development builds, preview builds, or production builds.
Environment variable size limitations
Environment variable value size is limited to 32 KiB for environment variables with secret visibility and 4 KiB for other visibility types.
Environment variable quota per Expo account and project
You can create up to 150 account-wide environment variables for each Expo account and 200 project-specific environment variables for each app.
Custom environment limitations
Custom environments are limited to 10 per project. When creating a custom environment, an environment name can contain letters, digits, underscores and hyphens, and must be between 3-100 characters.
Environment variables with secret visibility are not readable outside EAS servers
Environment variables with secret visibility are not readable outside of EAS servers and cannot be pulled locally for development or bundled into app JavaScript code for updates.
eas env:pull command for local development
The eas env:pull command pulls environment variables from EAS servers to the local .env file for development. The development environment is the default environment for this purpose, as it is used for development builds.
Create EAS environment variable with CLI
To create a new environment variable, use the command: eas env:set --name EXPO_PUBLIC_API_URL --value https://api.example.com --environment production --visibility plaintext. The --name parameter specifies the variable name, --value specifies the value, --environment specifies which environment (development, preview, production), and --visibility specifies the visibility type (plaintext, sensitive, or secret).
Pull environment variables for local development
Environment variables can be pulled locally for development using the eas env:pull command, which allows you to use the same set of environment variables for your local environment as for EAS jobs.
EAS Update with environment variables
To use environment variables with EAS Update, run the eas update command with the --environment flag: eas update --environment production. Only environment variables from the specified environment will be used during the update process.
EAS Hosting with environment variables
To use environment variables with EAS Hosting, run eas deploy with the --environment flag: eas deploy --environment production. If you need both client and server side environment variables, run eas env:pull --environment production, then npx expo export --platform web, then eas deploy --environment production in that order. Client-side variables must be plain text or sensitive, not secret.
Project-wide vs account-wide environment variables
Project-wide environment variables are specific to a single EAS project and managed via the Environment variables page in the project dashboard. Account-wide environment variables are available across all projects in an EAS account and managed via the Environment variables page in the account dashboard. Both types are available in jobs that run on EAS servers and updates, with account-wide variables combined with project-wide variables for a specific project.
EAS environment variable types
There are two types of EAS environment variables: Strings (standard key/value pairs used across builds, updates, workflows, and hosting) and Files (values uploaded as files such as google-services.json or certificates that are made available to jobs as file paths on the build runner).
Plain text visibility for environment variables
Plain text visibility is visible on the website, in EAS CLI, and in logs. This is suitable for non-sensitive values.
Sensitive visibility for environment variables
Sensitive visibility means the environment variable is obfuscated in EAS Build and Workflows job logs. Users can toggle to make them visible on the website, and they are also readable in EAS CLI.
Secret visibility for environment variables
Secret visibility means the environment variable is not readable outside of EAS servers, including on the website and in EAS CLI. Secrets are obfuscated in EAS Build and Workflows job logs. Secret environment variables are intended to provide values to EAS Build or Workflows jobs to alter how a job runs, such as setting an NPM_TOKEN to install private packages from npm. Secrets do not provide additional security for values embedded in the application itself.
Client-side environment variables visibility
Anything included in client-side code should be considered public and readable to any individual that can run your app. When using environment variables with EAS Hosting, client-side variables must be plain text or sensitive, not secret.
Environment variables scoped to single vs multiple environments
Environment variables can be assigned to multiple environments and have the same value across all of them, or be created for a single environment.
EAS environment variables overview
EAS environment variables are configured in EAS via the EAS CLI or on the expo.dev dashboard and can be accessed by EAS Build, EAS Workflows, and your local machine via EAS CLI. They solve the problems of keeping configuration in one place for cloud builds, updates, and workflows without committing .env files; separating values by environment (development, preview, production) while reusing names; controlling visibility so only the right surfaces can read each value; and applying the same set locally or inside CI/CD.
Local .env files not available for remote jobs
During local development, environment variables are loaded from local .env or .env.local files. These files are generally excluded from version control and are not available for jobs that run on a remote server, such as EAS Build and EAS Workflows.
Default EAS environments
By default, EAS supports three environments for environment variables: development, preview, and production. Custom environment names are available on Enterprise and Production plans.
EAS free access limits and reset period
Expo Application Services (EAS) offers free access to a limited quantity of low-priority builds on EAS Build and free updates with EAS Update. These limits reset monthly.
Monthly active user (MAU) definition for EAS Update billing
A monthly active user (MAU) is a unique user of an app that downloads at least one update via EAS Update within a single monthly billing period.
Expo Application Services (EAS)
Expo Application Services (EAS) is a set of services that complement the Expo framework in each step of the development process.
Reconnecting after Supabase disconnect
After you disconnect, connect no longer sees a linked project, so it creates a new one. To point the app back at the same project, run 'connect --link' with its reference ID.
EAS CLI Supabase integration command
Run 'eas integrations:supabase:connect' to automate the standard Supabase setup, which authorizes your Supabase account, creates or links a project, installs the SDK, and writes environment variables. To use an existing Supabase project, run 'eas integrations:supabase:connect --link <project-ref-or-url>'.
eas integrations:supabase:connect behavior
The connect command opens your browser to authorize Supabase, asks which organization to use if your account has more than one, asks for a region from Americas, Europe/Middle East/Africa, or Asia Pacific, installs @supabase/supabase-js and expo-sqlite with its config plugin, and writes EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY to .env.local and EAS environment variables across Production, Preview, and Development environments. Re-running connect is safe and prompts before overwriting environment variables.