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.
- 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_workerThe 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:edit3. 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
endSend 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
| Gem | Scope |
|---|---|
| web-push | Sending 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. |
| noticed | Notification 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)
endStubbing 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.
Keep reading
Service worker push events, done right
push and notificationclick handlers, focusing open tabs, action buttons, subscription changes and pitfalls.
VAPID keys for web push, explained
What the key pair does, how the signed JWT works, how to generate keys and why rotation breaks lists.
The Web Push protocol, explained
RFC 8030 delivery headers, RFC 8291 encryption, RFC 8292 VAPID, status codes and payload size.
PWA push notifications on iOS and Android
Manifest, service worker, iPhone Home Screen requirement, Android install, badging and a setup checklist.
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.