Skip to content
Developers · 8 min read

Push notifications in SvelteKit

SvelteKit has first-class service worker support: put a file at src/service-worker.js and it’s bundled and registered for you. That makes push straightforward — as long as you know where the worker lives and how to test it.

Updated By the Pusharoo team
In short
  • SvelteKit auto-registers src/service-worker.js (served at /service-worker.js) unless you disable it.
  • Add push and notificationclick listeners there, alongside any caching code.
  • Subscribe from a button with the Push API; save the subscription in a +server.js endpoint.
  • Test push in a production build (npm run build && npm run preview).

1. The service worker

// src/service-worker.js /// <reference types="@sveltejs/kit" /> self.addEventListener('push', (event) => { const d = event.data ? event.data.json() : {}; event.waitUntil(self.registration.showNotification(d.title ?? 'Update', { body: d.body, icon: '/icon-192.png', data: { url: d.url ?? '/' } })); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url)); });

Use the $service-worker module (build, files, version) if you also want offline caching. If you set kit.serviceWorker.register: false, register the worker yourself.

2. Subscribe from a component

<script> import { PUBLIC_VAPID_KEY } from '$env/static/public'; let state = $state('idle'); async function enable() { const reg = await navigator.serviceWorker.ready; if ((await Notification.requestPermission()) !== 'granted') return (state = 'denied'); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: PUBLIC_VAPID_KEY }); await fetch('/api/push', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(sub) }); state = 'on'; } </script> <button onclick={enable} disabled={state === 'on'}>🔔 Notify me</button>

That’s Svelte 5 syntax; in Svelte 4 use let state = 'idle' and on:click.

3. Save it on the server

// src/routes/api/push/+server.js import { json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; export async function POST({ request }) { const sub = await request.json(); await db.upsertSubscription(sub.endpoint, sub.keys.p256dh, sub.keys.auth); return json({ ok: true }, { status: 201 }); }

Send with the web-push package from a form action, endpoint or background job, keeping the private VAPID key in $env/static/private. 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.

Testing tips

  • Service workers need HTTPS or localhost.
  • In development, SvelteKit may register the worker as an ES module; production builds produce a standard worker. Verify push on npm run build && npm run preview.
  • In Chrome DevTools → Application → Service workers, use “Push” to fire a test event.
  • If an old worker sticks around, tick “Update on reload” while developing (testing guide).

Deploying: caching and adapters

SvelteKit emits the worker as /service-worker.js. Whatever adapter you use (Node, Vercel, Netlify, Cloudflare), make sure that file is served with a no-cache policy so updates reach users quickly — browsers check for updates, but a CDN that caches the file for a day delays every fix. Sending notifications needs a server or scheduled job; on serverless adapters, run large sends from a queue or a separate worker rather than inside a request.

A tiny push store

// src/lib/push.svelte.js — shared state across components (Svelte 5 runes) export const push = $state({ status: 'unknown' }); export async function refresh() { if (!('PushManager' in window)) return (push.status = 'unsupported'); const reg = await navigator.serviceWorker.ready; push.status = (await reg.pushManager.getSubscription()) ? 'on' : Notification.permission; }

Call refresh() in onMount in your layout so every component sees the same state.

iPhone and installability

On iPhone, push requires a Home Screen install, which requires a web app manifest with display: standalone and icons. Put manifest.webmanifest and icons in static/, link them from src/app.html, and show iPhone visitors Add-to-Home-Screen steps instead of a push button that can’t work in a Safari tab (iPhone steps).

Using a hosted service instead

With Pusharoo you don’t need your own sender. Add the snippet to src/app.html before %sveltekit.head%. Then either put Pusharoo’s sw.js in static/ (served at /sw.js) and delete src/service-worker.js, or keep your worker for caching and load Pusharoo inside it:

// top of src/service-worker.js (works in production builds) self.PUSHAROO_SITE = 'pk_your_site_key'; importScripts('https://pusharoo.com/sw.js');

…with data-sw="/service-worker.js" on the snippet. Don’t run two workers on the same scope — the last one registered wins.

Questions

Does SvelteKit register service workers automatically?

Yes, if src/service-worker.js exists, unless kit.serviceWorker.register is set to false.

Can I use web push with SvelteKit on Vercel or Netlify?

Yes. The worker is a static asset and sending happens from your server code or a separate job.

Why does importScripts fail in dev?

Module workers don’t support importScripts; SvelteKit can use a module worker in development. Test in a production build.

Can I disable SvelteKit’s automatic service worker registration?

Yes: set kit.serviceWorker.register to false in svelte.config.js and register the worker yourself — useful when a push provider’s snippet registers it.

Where do I keep VAPID keys in SvelteKit?

The public key can go in $env/static/public (PUBLIC_ prefix); the private key belongs in $env/static/private or $env/dynamic/private and must only be imported in server files.

More in Developers: The Web Push API in JavaScript: a working example · VAPID keys for web push, explained · The Web Push protocol, explained · 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 · Angular push notifications with SwPush · All guides

Free for life. No limits.

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