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.
- 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.
Keep reading
Push notifications in Vue 3 and Nuxt
A usePush composable, sw.js in public/, vite-plugin-pwa injectManifest and Nuxt head config.
Web push notifications in React and Next.js
Service worker in public/, a subscribe hook, a route handler with web-push, env keys and iPhone notes.
Service worker push events, done right
push and notificationclick handlers, focusing open tabs, action buttons, subscription changes and pitfalls.
Web push notifications with Node.js
Install web-push, generate keys, save subscriptions with Express, send with TTL and clean up dead endpoints.
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.