React-Native Expo Rails Heroku

Deploy Expo Router React Native Web app on Heroku

19 Nov 2022
2 minutes read

Expo Router “brings the best routing concepts from the web to native iOS and Android apps” and is a great solution when building React Native apps for web too.

Getting it to run on Heroku (together with a Ruby on Rails api-only application), is not really straigforward as of now though, so here is a small tutorial:

Have your Expo application inside the Rails directory structure

We chose app/frontend, but you can put it into other locations too.

Use the right Heroku buildpacks

Make sure that heroku/ruby and heroku/nodejs buildpacks are used for your project.

Set up a build script for your Expo project

Your Expo project needs to be built into a bunch of static files on each deploy automatically and placed inside a folder of your choice (we use /expo-spa):

  "build": "cd app/frontend && yarn && yarn expo export --platform web --output-dir ../../public/expo_build/"

This will output the following folders inside /public on each deploy:

└── expo_build/
    ├── assets/
    │   ├── node_modules/
    │   ├── some-hash-1
    │   ├── …
    │   └── some-hash-n
    ├── bundles/
    │   └── web-some-hash.js
    └── index.html

Serve the Expo build static files via Rails

To serve your build artifacts as static files via Rails, first create a controller expo_build_controller.rb. It has to include ActionController::MimeResponds manually, if you are using Rails in API only mode:

class ExpoBuildController < ApplicationController
  include ActionController::MimeResponds

  def index
    respond_to do |format|
      format.html { render body: Rails.root.join('expo-spa/index.html').read }

  def bundles
    respond_to do |format|
      format.js { render body: Rails.root.join("expo-spa/bundles/#{request.params[:path]}.js").read }

  def assets
    render body: Rails.root.join("expo-spa/assets/#{request.params[:path]}.#{request.params[:format]}").read

Now you have to route requests to the static files Heroku will build when deploying:

# Serve Expo React Native Web SPA
get '*path', to: 'expo_build#index', constraints: lambda { |request|
  !request.xhr? && request.format.html? # Prevents API calls from being redirected to the SPA
get '/bundles/*path', to: 'expo_build#bundles'
get '/assets/*path', to: 'expo_build#assets'