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

snuggly-nudger

v0.1.0

Published

Reusable client and server utilities for update notifications and in-app feedback reporting.

Readme

snuggly-nudger

npm version license

snuggly-nudger is a lightweight React + server utility package extracted from https://catacombs.9lives.quest for a snuggly approach to:

  • polling your deployed app version and showing update notifications
  • collecting feedback/bug reports in a reusable modal
  • providing server handlers for /api/version and /api/report

It is designed for modern full-stack apps (for example, Next.js App Router on Vercel), while staying framework-agnostic on the server side through standard Web Request/Response handlers.

The flow the original is built on uses Vercel serverless functions to interact with a Neon psql database (Neon is super cute, btw) on a droplet to post bug reports to a private channel in my discord server so I can click a button to create a github issue in the offending repository. Since I have a bunch of projects I'm prototyping at once, this is how I chose to centralize feedback from all of them. I'm moving it part of it here for re-use in other apps.

Install

npm install snuggly-nudger react

Package Exports

The main entry snuggly-nudger/client re-exports everything for convenience. If you are the kind of person who still side-eyes bundle size reports because you've been hurt before, import only what you need from granular subpaths instead (your bundler should tree-shake the barrel too when configured correctly):

import { useVersionPoll } from 'snuggly-nudger/client/useVersionPoll';
import { UpdateAvailableToast } from 'snuggly-nudger/client/UpdateAvailableToast';

Available subpaths: client/useVersionPoll, client/UpdateAvailableToast, client/FeedbackModal, client/ModalCloseButton, client/useIsMobile.

snuggly-nudger/client

  • useVersionPoll(currentVersion, options?)
  • UpdateAvailableToast
  • FeedbackModal
  • ModalCloseButton
  • useIsMobile(breakpoint?)

Type exports:

  • UseVersionPollOptions
  • UpdateAvailableToastProps
  • FeedbackModalProps
  • ModalCloseButtonProps

snuggly-nudger/server

  • createVersionHandler({ version })
  • createReportHandler({ projectId, reportsApiUrl? })

Type exports:

  • VersionHandlerConfig
  • ReportHandlerConfig

Client Usage

import { useState } from 'react';
import { FeedbackModal, UpdateAvailableToast, useVersionPoll } from 'snuggly-nudger/client';

declare const __APP_VERSION__: string;

export function App() {
  const [showFeedback, setShowFeedback] = useState(false);
  const { updateAvailable, dismissUpdate } = useVersionPoll(__APP_VERSION__);

  return (
    <>
      {updateAvailable && (
        <UpdateAvailableToast
          deployedVersion={updateAvailable}
          onRefresh={() => window.location.reload()}
          onDismiss={dismissUpdate}
        />
      )}

      <button type="button" onClick={() => setShowFeedback(true)}>
        Send Feedback
      </button>

      {showFeedback && (
        <FeedbackModal
          version={__APP_VERSION__}
          appMetadata={{ route: window.location.pathname }}
          appMetadataLabel="App State"
          onClose={() => setShowFeedback(false)}
        />
      )}
    </>
  );
}

useVersionPoll options

useVersionPoll(currentVersion, {
  apiPath: '/api/version',
  pollIntervalMs: 5 * 60 * 1000,
});

While the tab is hidden, scheduled interval polls are skipped; an extra check still runs when the tab becomes visible again. No need to wake up the network just because someone left a tab open next to twelve others.

Lazy-loading FeedbackModal (Next.js)

To avoid loading the modal until it is actually needed, because not every page load needs the whole kitchen sink:

import dynamic from 'next/dynamic';

const FeedbackModal = dynamic(
  () => import('snuggly-nudger/client/FeedbackModal').then((m) => m.FeedbackModal),
  { ssr: false }
);

Server Usage

/api/version

import { createVersionHandler } from 'snuggly-nudger/server';
import pkg from '../../../package.json';

export const { GET } = createVersionHandler({ version: pkg.version });

/api/report

import { createReportHandler } from 'snuggly-nudger/server';

export const { POST } = createReportHandler({
  projectId: 'my-app',
});

Report Handler Architecture

createReportHandler validates and sanitizes client payloads before proxying to your reports service. In other words: it tries to keep the useful signal and filter out the nonsense before your backend has to deal with it.

Request bodies are limited to 64 KiB by default (reject with 413). The upstream fetch uses a 15s timeout by default (response 502 with Upstream request timed out). Override via maxBodyBytes / upstreamTimeoutMs on the handler config if needed.

Resolution order for destination base URL:

  1. reportsApiUrl passed to createReportHandler(...)
  2. process.env.REPORTS_API_URL

Final upstream URL:

{reportsApiUrl}/api/reports

Request payload forwarded upstream:

{
  "projectId": "my-app",
  "description": "sanitized message",
  "version": "safe-version-string",
  "category": "bug",
  "metadata": {
    "app": {},
    "browser": {},
    "device": {}
  }
}

Only app, browser, and device metadata objects are forwarded.

Environment Variables

See .env.example for expected values. It is brief, on purpose, and does not require a decoding ring:

  • REPORTS_API_URL - Base URL for your reports ingestion service

Notes

  • FeedbackModal sends metadata keys as app, browser, and device.
  • createReportHandler validates type (feedback, bug, other) and sanitizes message + version before proxying.
  • The package does not implement app-specific state restoration on refresh; pass your own onRefresh callback. We provide the nudge, not a full state-management philosophy.

Demo App

A runnable demo is available in demo/ and shows the whole thing working end-to-end, without requiring interpretive dance:

  • update polling UI
  • feedback modal submission
  • /api/version, /api/report, and a mock /api/reports upstream endpoint