Skip to content
Developers · 9 min read

Web push notifications in Ruby on Rails

Rails 7.2 and 8 ship PWA scaffolding — a manifest and a service-worker view — which makes web push feel native to the framework. Add the web-push gem, a subscriptions table and a job, and you’re sending.

Updated By the Pusharoo team
In short
  • Rails 7.2+ generates app/views/pwa/service-worker.js and a commented route for it.
  • The web-push gem sends VAPID-signed, encrypted messages with WebPush.payload_send.
  • Rescue WebPush::ExpiredSubscription and WebPush::InvalidSubscription to delete dead rows.
  • Newer community gems (such as action_push_web) wrap the whole flow; check their maturity first.

1. Turn on the PWA routes

# config/routes.rb (generated commented-out in Rails 7.2+ apps) get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker

The worker lives in app/views/pwa/service-worker.js and is served at /service-worker, so its scope covers the whole app. Register it from your layout’s JavaScript with navigator.serviceWorker.register('/service-worker'). Add the push and notificationclick listeners shown below to that file.

// sw.js — show the notification and open the link on click self.addEventListener('push', e => { const d = e.data ? e.data.json() : {}; e.waitUntil(self.registration.showNotification(d.title || 'Update', { body: d.body, icon: '/icon-192.png', data: { url: d.url || '/' } })); }); self.addEventListener('notificationclick', e => { e.notification.close(); e.waitUntil(clients.openWindow(e.notification.data.url)); });

2. Keys and the web-push gem

# Gemfile gem "web-push" # once, in a console — then store in credentials key = WebPush.generate_key key.public_key # give this to the browser key.private_key # keep secret: bin/rails credentials:edit

3. Store subscriptions

bin/rails g model PushSubscription user:references endpoint:string:uniq p256dh:string auth:string # app/controllers/push_subscriptions_controller.rb class PushSubscriptionsController < ApplicationController def create s = params.require(:push_subscription) Current.user.push_subscriptions.find_or_initialize_by(endpoint: s[:endpoint]) .update!(p256dh: s.dig(:keys, :p256dh), auth: s.dig(:keys, :auth)) head :created end end

Send the subscription JSON wrapped as {push_subscription: sub} with the CSRF token from csrf_meta_tags in an X-CSRF-Token header.

4. Send from Active Job

class WebPushJob < ApplicationJob def perform(subscription, title:, body:, path: "/") WebPush.payload_send( message: { title:, body:, url: path }.to_json, endpoint: subscription.endpoint, p256dh: subscription.p256dh, auth: subscription.auth, vapid: { subject: "mailto:ops@example.com", public_key: Rails.application.credentials.dig(:vapid, :public_key), private_key: Rails.application.credentials.dig(:vapid, :private_key) }, ttl: 24.hours.to_i, urgency: "normal") rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription subscription.destroy end end user.push_subscriptions.each { |s| WebPushJob.perform_later(s, title: "New comment", body: "Ana replied to your post", path: "/posts/42") }

Push services answer 201 when they accept a message. 404 or 410 means the subscription is gone — delete it. 413 means the payload is too large (keep it under about 4 KB), 429 means slow down. Details in the Web Push protocol.

Gem options

GemScope
web-pushSending only — encryption, VAPID, headers. Mature and widely used.
action_push_web (community)Generators for models, jobs, a service worker and JS; young projects with more than one fork — review before adopting.
noticedNotification framework with delivery methods, including web push.

Turbo and Hotwire notes

  • Register the worker once (for example in application.js); Turbo navigation won’t re-run it unnecessarily.
  • Put the “Enable notifications” button in a Stimulus controller so the click handler survives Turbo visits.
  • On iPhone, a Home Screen install is required; the manifest Rails generates helps (iOS rules).

Hotwire Native and the App Store

If you wrap your Rails app with Hotwire Native for iOS or Android, notifications inside those native shells use APNs and FCM through native code, not the web Push API. Web push covers the browser and installed-PWA versions of the app. Many teams run both: the same Rails model can hold web subscriptions and native device tokens with a platform column.

Testing the job

# test/jobs/web_push_job_test.rb test "destroys expired subscriptions" do sub = push_subscriptions(:one) WebPush.stub(:payload_send, ->(**) { raise WebPush::ExpiredSubscription.new(Struct.new(:body).new(""), "host") }) do WebPushJob.perform_now(sub, title: "t", body: "b") end assert_not PushSubscription.exists?(sub.id) end

Stubbing the gem keeps tests fast and offline; check the exception constructor for the gem version you use.

When not to build it

Everything above is the do-it-yourself route: you own the keys, storage, sending queue and cleanup. If what you need is marketing-style notifications to anonymous visitors in your Rails app — a branded prompt, segments, schedules, A/B tests, automations, iPhone Home Screen handling — Pusharoo does that for free: add the snippet to your base template and upload sw.js. You can still call Pusharoo’s REST API from Rails for transactional sends (POST /api/v1/notify) and receive signed webhooks when people subscribe, so the two approaches mix well.

To combine with an existing Rails worker, add self.PUSHAROO_SITE = 'pk_…'; importScripts('https://pusharoo.com/sw.js'); at the top of service-worker.js and set data-sw="/service-worker" on the snippet, as the docs describe for existing workers.

Questions

Does Rails 8 include web push?

Rails 7.2+ generates PWA files (manifest and service worker view) and routes for them; the push sending itself comes from a gem such as web-push.

Which Ruby gem should I use for web push?

web-push is the established sending library. Higher-level gems like noticed or community action_push_web add structure on top.

Does it work on iPhone?

Yes, for users who add your app to the Home Screen on iOS 16.4+ and allow notifications there.

Where do I put the VAPID keys in Rails?

Store them in encrypted credentials (bin/rails credentials:edit) or environment variables, and expose only the public key to the page, for example via a meta tag.

Can I use Solid Queue to send web push?

Yes. Any Active Job backend works — Solid Queue, Sidekiq or GoodJob. Send each subscription as its own job or batch them per user.

More in Developers: The Web Push API in JavaScript: a working example · Web push notifications with Node.js · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Web push notifications in React and Next.js · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push in Python with pywebpush · Django push notifications to the browser · Web push in Go with webpush-go · Angular push notifications with SwPush · Push notifications in Vue 3 and Nuxt · Push notifications in SvelteKit · All guides

Free for life. No limits.

Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.