npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@visionpush/web-sdk

v1.0.1

Published

VisionPush Web Push SDK — VAPID-based push notifications with Safari support

Downloads

335

Readme

@visionpush/web-sdk

VisionPush Web Push SDK for browsers — TypeScript-first, RFC 8030 compliant, with full Safari Web Push support (macOS 13+ / iOS 16.4+) and a first-class React/Next.js integration.

Features

  • TypeScript-first — complete type definitions, strict mode, no any
  • RFC 8030 + VAPID — standard Web Push with application server authentication
  • Safari Web Push — works natively on Safari 16.4+ (macOS 13, iOS 16.4) without any special casing
  • Service Worker managed — background push handling, notification display, click tracking
  • Permission UX best practices — never prompt on page load, detect sticky "denied" state, track dismissals
  • Analytics — track notification delivered / clicked / dismissed events
  • React hookuseVisionPush() with reactive state and stable callbacks
  • Next.js App Router compatible — SSR-safe, no window access on the server
  • ESM + CJS builds — works in all modern bundlers (Vite, webpack, esbuild, Rollup)
  • Clean architecture — five focused classes with clear boundaries

Installation

npm install @visionpush/web-sdk
# or
pnpm add @visionpush/web-sdk
# or
yarn add @visionpush/web-sdk

Service Worker Setup

Copy the service worker script to your web root:

cp node_modules/@visionpush/web-sdk/public/service-worker.js public/visionpush-sw.js

The file must be served from the same origin as your app and at the path you configure in serviceWorkerUrl (default: /visionpush-sw.js).

Existing service worker? Import the VisionPush worker from yours:

// your-sw.js
importScripts('/visionpush-sw.js');
// … your own logic below

Quick Start

Vanilla JS / TypeScript

import { VisionPushClient } from '@visionpush/web-sdk';

const vp = new VisionPushClient({
  appId: 'your-app-uuid',
  apiKey: 'vp_live_xxxxxxxxxxxxxxxxxxxxxxxx',
  vapidPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
});

// Initialize once when the app loads.
await vp.initialize();

// Request permission from a user gesture (e.g., button click).
document.getElementById('subscribe-btn').addEventListener('click', async () => {
  try {
    const result = await vp.requestPermission();
    if (result.granted) {
      console.log('Subscribed! Subscriber ID:', vp.getSubscriberId());
    }
  } catch (err) {
    if (err.code === 'PERMISSION_DENIED') {
      showPermissionDeniedBanner();
    }
  }
});

// React to incoming notifications (foreground).
vp.on('notification:clicked', (event) => {
  window.open(event.payload.url, '_blank');
});

vp.on('notification:received', (event) => {
  showInAppNotificationToast(event.payload);
});

React

import { useVisionPush } from '@visionpush/web-sdk/react';

export function NotificationBell() {
  const { isPermitted, isSubscribed, requestPermission, error, isReady } = useVisionPush({
    appId: process.env.NEXT_PUBLIC_VP_APP_ID!,
    apiKey: process.env.NEXT_PUBLIC_VP_API_KEY!,
    vapidPublicKey: process.env.NEXT_PUBLIC_VP_VAPID_KEY!,
    onNotification: (event) => {
      if (event.type === 'notification:clicked') {
        router.push(event.payload.url ?? '/notifications');
      }
    },
  });

  if (!isReady) return null;

  return (
    <button
      onClick={requestPermission}
      disabled={isSubscribed}
      aria-label={isSubscribed ? 'Notifications enabled' : 'Enable notifications'}
    >
      {isSubscribed ? '🔔 Subscribed' : '🔕 Enable Notifications'}
      {error && <span className="error">{error.message}</span>}
    </button>
  );
}

Next.js App Router

Wrap the hook in a client component (the SDK uses browser APIs):

// app/components/PushProvider.tsx
'use client';

import { useVisionPush } from '@visionpush/web-sdk/react';

export function PushProvider({ children }: { children: React.ReactNode }) {
  useVisionPush({
    appId: process.env.NEXT_PUBLIC_VP_APP_ID!,
    apiKey: process.env.NEXT_PUBLIC_VP_API_KEY!,
    vapidPublicKey: process.env.NEXT_PUBLIC_VP_VAPID_KEY!,
    autoSubscribeIfGranted: true,
  });

  return <>{children}</>;
}

// app/layout.tsx
import { PushProvider } from './components/PushProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <PushProvider>{children}</PushProvider>
      </body>
    </html>
  );
}

Configuration

VisionPushClientOptions

| Option | Type | Default | Description | |---|---|---|---| | appId | string | required | UUID of your VisionPush application | | apiKey | string | required | Public API key (prefix vp_live_ or vp_test_) | | vapidPublicKey | string | — | VAPID public key (URL-safe Base64). Fetched from API if omitted. | | apiBaseUrl | string | https://api.visionpush.de/v1 | Override for staging or self-hosted deployments | | serviceWorkerUrl | string | /visionpush-sw.js | Path to the service worker script | | serviceWorkerScope | string | / | Service worker scope | | debug | boolean | false | Enable verbose console logging | | maxRetries | number | 3 | Maximum API request retries | | retryDelayMs | number | 1000 | Initial retry delay (ms); uses exponential back-off |


API Reference

VisionPushClient

Lifecycle

const vp = new VisionPushClient(options);

await vp.initialize();    // Must be called before any other method.
vp.destroy();             // Tear down listeners and cancel pending requests.

Permission & Subscription

await vp.requestPermission();  // Shows browser permission prompt; subscribes on grant.
await vp.unsubscribe();        // Removes subscription from browser and backend.

vp.getPermissionStatus();      // 'default' | 'granted' | 'denied'
vp.isPermitted();              // true if granted
vp.isSubscribed();             // true if active push subscription exists
vp.getSubscriberId();          // VisionPush subscriber ID or null
vp.isReady();                  // true after successful initialize()

Events

// Subscribe to an event — returns an unsubscribe function.
const off = vp.on('notification:clicked', (event) => { ... });
off(); // Remove the listener.

// One-time listener.
vp.once('notification:received', (event) => { ... });

// Remove a specific listener.
vp.off('notification:clicked', myHandler);

Available events:

| Event | Payload type | Description | |---|---|---| | notification | NotificationEvent | All notification events (union) | | notification:received | NotificationReceivedEvent | Push message received while page is open | | notification:clicked | NotificationClickedEvent | User clicked the notification | | notification:dismissed | NotificationDismissedEvent | User dismissed without clicking | | permission:changed | { status: PermissionStatus } | Browser permission status changed | | subscription:created | PushSubscriptionData | New push subscription registered | | subscription:deleted | { endpoint: string } | Subscription removed | | error | VisionPushError | SDK error (non-fatal) |


useVisionPush(options) hook

| Return value | Type | Description | |---|---|---| | readyState | 'idle' \| 'initializing' \| 'ready' \| 'error' | SDK initialization state | | isReady | boolean | Shorthand for readyState === 'ready' | | permissionStatus | PermissionStatus | Current notification permission | | isPermitted | boolean | Shorthand for permissionStatus === 'granted' | | isSubscribed | boolean | Active push subscription exists | | subscriberId | string \| null | VisionPush subscriber ID | | error | VisionPushError \| null | Last SDK error | | requestPermission | () => Promise<boolean> | Request permission + subscribe | | unsubscribe | () => Promise<void> | Remove subscription | | client | VisionPushClient \| null | Direct SDK access |

Additional options for the hook:

| Option | Type | Default | Description | |---|---|---|---| | autoSubscribeIfGranted | boolean | true | Re-subscribe silently if permission already granted | | onNotification | (event) => void | — | Called for every notification event |


useVisionPushEvent(client, event, handler) hook

Advanced hook for subscribing to a specific SDK event within a component:

useVisionPushEvent(client, 'notification:clicked', (event) => {
  router.push(event.payload.url ?? '/');
});

Error Handling

All SDK errors extend VisionPushError and expose a code property for programmatic handling:

import {
  VisionPushError,
  PermissionDeniedError,
  PermissionDismissedError,
  SubscriptionError,
  ApiError,
  NetworkError,
} from '@visionpush/web-sdk';

try {
  await vp.requestPermission();
} catch (err) {
  if (err instanceof PermissionDeniedError) {
    // User has explicitly denied — don't ask again.
    showSettingsGuide();
  } else if (err instanceof PermissionDismissedError) {
    // Prompt was closed — we can try again later.
    scheduleReminderAfterDelay();
  } else if (err instanceof ApiError) {
    console.error(`API error ${err.statusCode}:`, err.message);
  } else if (err instanceof NetworkError) {
    showOfflineBanner();
  }
}

Error codes:

| Code | Class | Description | |---|---|---| | NOT_INITIALIZED | NotInitializedError | Method called before initialize() | | BROWSER_NOT_SUPPORTED | BrowserNotSupportedError | Browser lacks required APIs | | SERVICE_WORKER_REGISTRATION_FAILED | ServiceWorkerRegistrationError | SW registration error | | PERMISSION_DENIED | PermissionDeniedError | User denied notifications | | PERMISSION_DISMISSED | PermissionDismissedError | User dismissed the prompt | | SUBSCRIPTION_FAILED | SubscriptionError | Push subscription creation failed | | API_ERROR | ApiError | VisionPush API returned an error | | NETWORK_ERROR | NetworkError | Network failure | | INVALID_CONFIG | InvalidConfigError | Bad constructor options |


Browser Support

| Browser | Version | Notes | |---|---|---| | Chrome | 50+ | Full support | | Edge | 79+ | Full support (Chromium-based) | | Firefox | 44+ | Full support | | Safari | 16.4+ | macOS 13 Ventura, iOS 16.4 — W3C Push API | | Samsung Internet | 5.0+ | Full support | | Opera | 37+ | Full support |


Architecture

VisionPushClient          ← Main orchestrator; event emitter
  ├── ServiceWorkerManager  ← SW registration, updates, messaging
  ├── PermissionManager     ← Permission state, prompt flow, change watcher
  ├── PushSubscriptionManager ← VAPID subscribe/unsubscribe, cache
  ├── ApiClient             ← Typed fetch wrapper with retry + auth
  └── NotificationTracker   ← Deduped event reporting

The service worker (public/service-worker.js) runs independently in the browser's SW thread and communicates with the SDK via BroadcastChannel (with a postMessage fallback for older Safari).


Publishing Checklist

  1. Copy public/service-worker.js to your public/ directory.
  2. Add NEXT_PUBLIC_VP_APP_ID, NEXT_PUBLIC_VP_API_KEY, and NEXT_PUBLIC_VP_VAPID_KEY to your environment.
  3. Ensure the SW file is served with Service-Worker-Allowed header if the scope exceeds its directory.
  4. Verify HTTPS — Web Push requires a secure context (or localhost).

License

MIT © VisionLab