Skip to content
Developers · 8 min read

Push notifications in Vue 3 and Nuxt

Vue doesn’t ship a push API of its own — and doesn’t need to. The browser’s Push API works the same in any framework. Here’s a clean Vue 3 composable, where the service worker goes in Vite and Nuxt, and how to use vite-plugin-pwa without losing push.

Updated By the Pusharoo team
In short
  • Put sw.js in public/ (Vite and Nuxt serve it at /sw.js).
  • Wrap permission, subscription and state in a usePush() composable; call subscribe() from a click.
  • With vite-plugin-pwa, use the injectManifest strategy to add push listeners to your own worker.
  • Nuxt: add scripts via app.head in nuxt.config, and guard browser-only code with onMounted.

1. A usePush composable

// composables/usePush.js import { ref, onMounted } from 'vue'; const VAPID_PUBLIC = import.meta.env.VITE_VAPID_PUBLIC; export function usePush() { const state = ref('loading'); // unsupported | default | denied | subscribed onMounted(async () => { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return (state.value = 'unsupported'); const reg = await navigator.serviceWorker.register('/sw.js'); state.value = (await reg.pushManager.getSubscription()) ? 'subscribed' : Notification.permission; }); async function subscribe() { const reg = await navigator.serviceWorker.ready; if (await Notification.requestPermission() !== 'granted') return (state.value = Notification.permission); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC }); await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) }); state.value = 'subscribed'; } return { state, subscribe }; }
<!-- NotifyButton.vue --> <script setup> import { usePush } from '@/composables/usePush'; const { state, subscribe } = usePush(); </script> <template> <button v-if="state === 'default'" @click="subscribe">🔔 Get notifications</button> <p v-else-if="state === 'denied'">Notifications are blocked in your browser settings.</p> </template>

2. The service worker

Files in public/ are copied as-is to the site root in both Vite and Nuxt, so public/sw.js becomes /sw.js with scope /:

// 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)); });

Using vite-plugin-pwa

The default generateSW strategy builds a Workbox worker for caching and doesn’t include your push handlers. Switch to injectManifest and write the worker yourself:

// vite.config.js import { VitePWA } from 'vite-plugin-pwa'; export default { plugins: [VitePWA({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.js', registerType: 'autoUpdate' })] }; // src/sw.js import { precacheAndRoute } from 'workbox-precaching'; precacheAndRoute(self.__WB_MANIFEST); self.addEventListener('push', e => { /* same as above */ });

For Nuxt, @vite-pwa/nuxt exposes the same options.

Nuxt specifics

  • Browser APIs only exist on the client: call them in onMounted or a .client.js plugin.
  • Add third-party scripts in nuxt.config:
    export default defineNuxtConfig({ app: { head: { script: [{ src: 'https://pusharoo.com/sdk/pusharoo.js', async: true, 'data-site': 'pk_your_site_key' }] } } });
  • Put the subscribe endpoint in server/api/push/subscribe.post.js if you send from Nitro.

Sending

Any Web Push library works: web-push for Node fits naturally if your Nuxt server or an API sends notifications. 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.

Development gotchas

  • Vite’s dev server serves public/sw.js but doesn’t bundle it; keep the worker plain JavaScript or use vite-plugin-pwa’s dev options.
  • Service workers survive reloads — tick “Update on reload” in DevTools while editing.
  • Hot module replacement doesn’t reload the worker; push handlers change only after the worker updates.
  • Test on localhost or HTTPS; a LAN IP over HTTP won’t allow service workers.

iPhone-aware UI

const isIOS = /iPhone|iPad/.test(navigator.userAgent); const installed = matchMedia('(display-mode: standalone)').matches; const state = computed(() => (isIOS && !installed) ? 'needs-home-screen' : push.state.value);

Show Home Screen steps for needs-home-screen rather than a button that fails silently (steps to show).

Sharing state with Pinia

If several components need the subscription state (a header bell, a settings page, an article footer), move the composable’s state into a Pinia store and initialise it once in App.vue or a Nuxt client plugin. That avoids registering the worker or calling getSubscription() repeatedly and keeps the UI consistent after someone subscribes in one place.

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 Vue or Nuxt 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 Vue or Nuxt for transactional sends (POST /api/v1/notify) and receive signed webhooks when people subscribe, so the two approaches mix well.

With Pusharoo, Pusharoo.subscribe() replaces the composable’s subscribe function and Pusharoo.state() its state — including the special ios-needs-home-screen state for iPhone visitors.

Questions

Does Vue have built-in push notifications?

No; use the browser Push API (as in the composable) or a service. Frameworks don’t change how web push works.

Where should sw.js go in a Vite project?

In public/, so it is served from the site root with full scope.

Can Vue push notifications reach iPhone?

Yes, on iOS 16.4+ once the site is added to the Home Screen and opened from there.

Can I use Firebase Messaging with Vue?

Yes, but it adds a Firebase project and its own service worker. For web-only push, the standard Push API with VAPID keys (as in the composable) is simpler.

How do I add push to a Quasar or Ionic Vue app?

In the web/PWA build, use the Push API as shown. Inside native Capacitor or Cordova shells, use the platform’s native push plugin instead.

More in Developers: The Web Push API in JavaScript: a working example · Service worker push events, done right · VAPID keys for web push, explained · The Web Push protocol, explained · Web push notifications with Node.js · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · 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 notifications in Ruby on Rails · Web push in Go with webpush-go · All guides

Free for life. No limits.

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