How to Build a Progressive Web App with Ruby on Rails 8: A Complete Guide

Chetan MittalChetan Mittal
10 min read

Progressive Web Apps (PWAs) represent the next generation of web applications, offering a blend of web and native app experiences.

With features like offline capabilities, push notifications, and installation prompts, PWAs provide a powerful way to enhance user engagement.

In this article, we’ll explore how to build a PWA using Ruby on Rails—focusing on Rails 8 and Hotwire—without relying on Webpacker or external libraries like Firebase.


What Makes a Progressive Web App Stand Out?

A Progressive Web App (PWA) combines the best of web and mobile apps, offering:

  1. Offline Functionality: Users can access the app even without an internet connection.

  2. Installability: PWAs can be added to the home screen, providing a native app-like experience.

  3. Push Notifications: Engage users by sending timely updates directly to their devices.

  4. Responsive Design: PWAs adapt seamlessly to various screen sizes and orientations.

To qualify as a PWA, an app must include:

  • A Web App Manifest: A JSON file with metadata about the app.

  • A Service Worker: A script to handle caching and offline features.

  • Secure HTTPS: PWAs must be served over HTTPS to ensure security.


Why Choose Ruby on Rails for Your PWA?

Rails 8 introduces a streamlined development experience with Hotwire (Turbo and Stimulus), eliminating the need for complex JavaScript bundlers like Webpacker. This makes Rails an excellent choice for building PWAs.

Hotwire’s Turbo Drive and Turbo Streams simplify real-time interactions, while Stimulus enhances interactivity with minimal JavaScript.

Key advantages of Rails 8 for PWAs:

  • Turbo Drive: Automatically enhances navigation for faster transitions.

  • Turbo Streams: Enables real-time updates without manual JavaScript coding.

  • Stimulus: Provides a simple way to add interactivity.

  • Server-Rendered Content: Reduces reliance on heavy front-end frameworks.


Step 1: Kickstart Your Rails Application

Start by creating a new Rails 8 application:

rails new pwa_demo
cd pwa_demo

Hotwire is included by default in Rails 8. Verify its presence:

  • Turbo: app/javascript/controllers

  • Stimulus: app/javascript/controllers/application.js


Step 2: Define Your App with a Web App Manifest

The manifest file is the foundation of a PWA. It defines the app’s name, icons, theme color, and display settings.

Adding a Manifest File

Create app/views/home/manifest.json.jbuilder:

{
  "name": "PWA Demo App",
  "short_name": "PWA Demo",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/assets/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/assets/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Add a route in config/routes.rb to serve the manifest:

Rails.application.routes.draw do
  root "home#index"
  get "/manifest.json", to: "home#manifest", defaults: { format: :json }
end

Create a HomeController to handle the manifest:

class HomeController < ApplicationController
  def index
  end

  def manifest
  end
end

Update app/views/layouts/application.html.erb to include the manifest:

<head>
  <meta name="theme-color" content="#000000">
  <link rel="manifest" href="/manifest.json">
</head>

Step 3: Enable Offline Support with a Service Worker

Service workers enable offline functionality by caching resources and intercepting network requests.

Creating a Service Worker

Add app/assets/javascript/service_worker.js:

const CACHE_NAME = "pwa-demo-cache-v1";
const urlsToCache = ["/", "/manifest.json"];

// Install event
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache);
    })
  );
});

// Fetch event
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

Register the service worker in app/javascript/application.js:

if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker.register("/service-worker.js").then((registration) => {
      console.log("ServiceWorker registered: ", registration);
    }).catch((error) => {
      console.error("ServiceWorker registration failed: ", error);
    });
  });
}

Add a route in config/routes.rb:

get "/service-worker.js", to: "home#service_worker"

Update HomeController to serve the service worker:

def service_worker
  respond_to do |format|
    format.js { render file: Rails.root.join("app/assets/javascript/service_worker.js") }
  end
end

Step 4: Engage Users with Push Notifications

Push notifications are a powerful feature for engaging users in your Progressive Web App (PWA).

You can implement push notifications using the webpush gem, which allows you to send notifications directly to users' devices.

For this, you will need to generate VAPID keys, set up Web Push configuration, and then send notifications.

Install the webpush Gem

To get started with push notifications, you need to install the webpush gem, which helps send push notifications to users.

Add the webpush gem to your Gemfile:

gem 'webpush'

Then run:

bundle install

Generate VAPID Keys

VAPID (Voluntary Application Server Identification) keys are required to authenticate your push notifications. These keys allow the push service to verify that the push request comes from your app.

To generate the VAPID keys, open the Rails console:

rails console

Then, run the following command to generate the VAPID keys:

vapid_key = Webpush.generate_key
puts "Public Key: #{vapid_key.public_key}"
puts "Private Key: #{vapid_key.private_key}"

This will output the Public Key and Private Key to the console. These keys will be used to configure your push notifications.

Set the VAPID Keys as Environment Variables

To securely store your VAPID keys, it's best to set them as environment variables, especially for production environments.

  1. In your .env file (for development), add the generated keys:
VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
  1. If you're deploying to production (e.g., Heroku), set the keys as environment variables in your server:
heroku config:set VAPID_PUBLIC_KEY=your_public_key_here
heroku config:set VAPID_PRIVATE_KEY=your_private_key_here

Configure Web Push in Rails

After storing the VAPID keys as environment variables, configure the webpush gem to use these keys.

Create a new initializer file in your Rails app at config/initializers/webpush.rb:

# config/initializers/webpush.rb

Webpush.configure do |config|
  config.vapid_public_key = ENV["VAPID_PUBLIC_KEY"]  # Fetch the public key from environment
  config.vapid_private_key = ENV["VAPID_PRIVATE_KEY"]  # Fetch the private key from environment
  config.vapid_subject = "mailto:your-email@example.com"  # Set the contact email for VAPID
end
  • config.vapid_public_key and config.vapid_private_key fetch the VAPID keys from the environment variables.

  • config.vapid_subject is an email address that identifies the sender for push notifications.

Sending Push Notifications

With the VAPID keys configured, you can now send push notifications to users. Here’s an example of how to send a push notification:

Create a method to send push notifications:

def send_push_notification(subscription, message)
  Webpush.payload_send(
    message: message,  # The message you want to send to the user
    endpoint: subscription[:endpoint],  # The user's subscription endpoint
    p256dh: subscription[:keys][:p256dh],  # The user's public key
    auth: subscription[:keys][:auth],  # The user's auth secret
    vapid: {
      subject: "mailto:your-email@example.com",  # Your contact email
      public_key: ENV["VAPID_PUBLIC_KEY"],  # The VAPID public key
      private_key: ENV["VAPID_PRIVATE_KEY"]  # The VAPID private key
    }
  )
end

In this method:

  • subscription is the data received from the client when they subscribe for push notifications.

  • message is the content of the push notification you want to send.

Subscribe Users to Push Notifications

You’ll need to handle the client-side subscription process. When a user opts in to receive push notifications, you need to subscribe them and store their subscription details (like the endpoint and keys).

On the client side, use JavaScript to request permission for push notifications and register a service worker. Once the user subscribes, send the subscription information to your Rails server to store and use for sending notifications.

Here's an example of how to subscribe the user in JavaScript:

if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.ready.then(function(registration) {
    registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(publicKey)
    }).then(function(subscription) {
      // Send subscription details to your server to store it
      fetch('/subscriptions', {
        method: 'POST',
        body: JSON.stringify(subscription),
        headers: {
          'Content-Type': 'application/json'
        }
      });
    });
  });
}

Testing Push Notifications

To test push notifications, you can use the Chrome Developer Tools:

  1. Open your app in Chrome.

  2. Open DevTools (right-click > Inspect).

  3. Navigate to the Application tab and look for Service Workers and Push Notifications.

  4. You can simulate push notifications from the Service Worker section and check if the push notification works as expected.


Step 5: Optimize Performance for Fast Loading

PWAs thrive on speed. Use Rails’ asset pipeline and caching mechanisms to minimize load times.

Asset Compression

Enable gzip compression for static assets. Add the rack-deflater gem to your Gemfile:

gem 'rack-deflater'

Update config/application.rb to include:

config.middleware.use Rack::Deflater

Browser Caching

Set far-future expiry headers for assets in production. Update config/environments/production.rb:

config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}

Lazy Loading

Rails 6+ supports native lazy loading for images. Use the loading="lazy" attribute in your views:

<%= image_tag "example.jpg", loading: "lazy" %>

Step 6: Test Your PWA on Multiple Devices

Testing ensures your PWA works seamlessly across devices and platforms. PWAs are designed to provide a consistent experience regardless of the user's device, so it’s essential to test it on multiple environments.

Google Lighthouse

Run a Lighthouse audit directly in Chrome DevTools to evaluate your app's performance, accessibility, and overall PWA compliance:

  1. Open the application in Chrome.

  2. Go to DevTools (right-click > Inspect or press Cmd + Option + I on macOS or Ctrl + Shift + I on Windows).

  3. Navigate to the "Lighthouse" tab in the DevTools panel.

  4. Click on "Generate report" after selecting the mobile or desktop configuration. This will provide you with a comprehensive performance report with suggestions for improvements.

Testing in Different Browsers and Devices

While Lighthouse is helpful, it’s equally important to test on real devices and browsers to verify how your app behaves in various scenarios:

  • Mobile devices: Test the PWA on both Android and iOS devices. Ensure that it installs correctly, works offline, and sends push notifications. PWAs on iOS have certain limitations, such as lacking support for service workers in Safari on older versions of iOS, so be mindful of these.

  • Cross-browser compatibility: Ensure the PWA works smoothly across browsers like Chrome, Firefox, Edge, and Safari. Pay attention to any issues with service workers or manifest file configurations that may arise in different environments.

  • Network conditions: Test the app with different network speeds (including offline) to see how well it handles caching and service worker-based offline behavior.

Handling Edge Cases

Ensure that edge cases such as slow connections, interrupted services, or failed push notifications are properly handled. For example:

  • Offline behavior: Make sure that important routes or pages are cached and available offline.

  • Push notifications fallback: When push notifications fail, provide a fallback UI to notify users.


Step 7: Deploy Your PWA to Production

Once you've completed testing, it's time to deploy your app. For PWAs to work as intended, they must be served over HTTPS. Here’s how to deploy your Rails 8 PWA:

Deploy to Heroku

Heroku is an easy platform for deploying Rails applications. To deploy your app:

  1. Initialize a Git repository if you haven't already:

     git init
     git add .
     git commit -m "Initial commit"
    
  2. Create a Heroku app and push your changes:

     heroku create pwa-demo
     git push heroku main
    
  3. Set up your environment variables for VAPID keys in Heroku:

     heroku config:set VAPID_PUBLIC_KEY=your_public_key
     heroku config:set VAPID_PRIVATE_KEY=your_private_key
    
  4. Run database migrations:

     heroku run rake db:migrate
    
  5. Once your app is live, test again on the deployed environment to ensure everything is working as expected.

Deploy to Other Platforms

For other hosting platforms like DigitalOcean, AWS, or your own servers, ensure that you’ve set up SSL/TLS certificates for HTTPS (using services like Let’s Encrypt for free SSL certificates).


Step 8: Maintain and Improve Your PWA

After deployment, you’ll want to continuously improve your PWA based on user feedback and performance metrics. Here are some strategies for maintaining a top-tier PWA:

Monitor Performance

Use Google Analytics or other tracking tools to monitor user engagement, load times, and offline usage. Analyzing this data helps in identifying areas for improvement.

Update Content and Push Notifications

Push notifications are an excellent way to re-engage users. Regularly update content on your app, and send notifications when there’s something new or valuable to share.

  • Timely and relevant notifications: Make sure your notifications are meaningful and don’t overwhelm users.

  • Segment your audience: You can send personalized push notifications based on user behavior and preferences, improving relevance.

Keep Dependencies Updated

Rails 8 and Hotwire offer a smooth development experience, but you should still periodically check for updates to your dependencies (especially security patches). Use tools like bundler-audit to scan for vulnerabilities in your gems.


Conclusion

Building a PWA with Ruby on Rails 8 provides a fast, modern, and robust solution for delivering a mobile app-like experience on the web.

With Hotwire’s Turbo and Stimulus, combined with simple and effective service worker integration, you can create a seamless experience that works across devices and platforms.

By following this guide, you’ve learned how to implement key features of a PWA, including offline support, push notifications, and app installability, all while maintaining the simplicity and productivity Rails offers.

Whether you’re building a simple web app or a complex user-facing product, PWAs are a great way to enhance engagement, retention, and performance without relying on large external JavaScript frameworks.

Good luck, and happy coding!

1
Subscribe to my newsletter

Read articles from Chetan Mittal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chetan Mittal
Chetan Mittal

I stumbled upon Ruby on Rails beta version in 2005 and has been using it since then. I have also trained multiple Rails developers all over the globe. Currently, providing consulting and advising companies on how to upgrade, secure, optimize, monitor, modernize, and scale their Rails apps.