{"id":49111186,"url":"https://github.com/piro0919/next-push","last_synced_at":"2026-04-21T05:01:44.305Z","repository":{"id":352768845,"uuid":"1216531316","full_name":"piro0919/next-push","owner":"piro0919","description":"Web Push notifications for Next.js App Router — React hooks, VAPID sender, Service Worker helpers, and CLI","archived":false,"fork":false,"pushed_at":"2026-04-21T04:18:17.000Z","size":734,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-21T04:39:30.660Z","etag":null,"topics":["hook","hooks","nextjs","npm-package","push-notifications","pwa","react","service-worker","typescript","vapid","web-push"],"latest_commit_sha":null,"homepage":"https://next-push.kkweb.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/piro0919.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-21T02:06:31.000Z","updated_at":"2026-04-21T04:18:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/piro0919/next-push","commit_stats":null,"previous_names":["piro0919/next-push"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/piro0919/next-push","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piro0919%2Fnext-push","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piro0919%2Fnext-push/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piro0919%2Fnext-push/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piro0919%2Fnext-push/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/piro0919","download_url":"https://codeload.github.com/piro0919/next-push/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piro0919%2Fnext-push/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32077837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T02:38:07.213Z","status":"ssl_error","status_checked_at":"2026-04-21T02:38:06.559Z","response_time":128,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hook","hooks","nextjs","npm-package","push-notifications","pwa","react","service-worker","typescript","vapid","web-push"],"created_at":"2026-04-21T05:01:43.518Z","updated_at":"2026-04-21T05:01:44.299Z","avatar_url":"https://github.com/piro0919.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# next-push\n\nWeb Push notifications for Next.js — client hooks, server sender, and Service Worker helpers with full VAPID support.\n\n[![npm](https://img.shields.io/npm/v/@piro0919/next-push.svg)](https://www.npmjs.com/package/@piro0919/next-push)\n[![license](https://img.shields.io/npm/l/@piro0919/next-push.svg)](./LICENSE)\n\n**🔗 [Live Demo](https://next-push.kkweb.io/)** — subscribe in Chrome/Edge, press \"Send test notification\", get a real notification.\n\n## Why\n\n- `web-push` is Node-only, weakly typed, and requires manual wiring into React and Next.js\n- OneSignal and FCM are overkill for many apps and lock you into a vendor\n- This package does all three sides (client / server / SW) for Next.js App Router with TypeScript-first APIs\n\n## Install\n\n```bash\npnpm add @piro0919/next-push\nnpx next-push init\n```\n\nThat's it — a working push demo is scaffolded at `/push-demo`.\n\n## Quick Start\n\n```tsx\n// app/push-toggle/page.tsx\n\"use client\";\nimport { usePush } from \"@piro0919/next-push\";\n\nexport default function PushToggle() {\n  const { subscription, subscribe, unsubscribe, permission } = usePush();\n  if (permission === \"denied\") return \u003cp\u003eBlocked\u003c/p\u003e;\n  return subscription\n    ? \u003cbutton onClick={unsubscribe}\u003eTurn off\u003c/button\u003e\n    : \u003cbutton onClick={subscribe}\u003eTurn on\u003c/button\u003e;\n}\n```\n\n```ts\n// app/api/push/route.ts\nimport { createPushHandler } from \"@piro0919/next-push/server\";\nimport { saveSubscription, deleteSubscription } from \"@/lib/db\";\n\nexport const { POST, DELETE } = createPushHandler({\n  onSubscribe: saveSubscription,\n  onUnsubscribe: deleteSubscription,\n});\n```\n\n```ts\n// wherever you want to send a push\nimport { sendPush } from \"@piro0919/next-push/server\";\nconst result = await sendPush(subscription, { title: \"Hello\", body: \"World\" });\nif (!result.ok \u0026\u0026 result.gone) await deleteSubscription(subscription.endpoint);\n```\n\n## Partial Install\n\n```bash\nnpx next-push init --send-only     # server-side only\nnpx next-push init --receive-only  # client + SW only\n```\n\n## API\n\n### `usePush(options?)`\n\n| Return | Type | Notes |\n|---|---|---|\n| `isSupported` | `boolean` | `false` during SSR and on unsupported browsers |\n| `permission` | `'default' \\| 'granted' \\| 'denied'` | |\n| `subscription` | `PushSubscriptionJSON \\| null` | |\n| `subscribe()` | `() =\u003e Promise\u003cPushSubscriptionJSON\u003e` | Requests permission and subscribes |\n| `unsubscribe()` | `() =\u003e Promise\u003cvoid\u003e` | |\n| `isSubscribing` | `boolean` | |\n| `error` | `Error \\| null` | |\n\n### `sendPush(subscription, payload, options?)`\n\nReturns a discriminated `SendResult`:\n- `{ ok: true, statusCode }` — delivered\n- `{ ok: false, gone: true, statusCode: 404 | 410 }` — subscription is dead, delete it\n- `{ ok: false, gone: false, error, statusCode? }` — other failure (transient or misconfig)\n\n### `createPushHandler({ onSubscribe, onUnsubscribe })`\n\nReturns `{ POST, DELETE }` ready to re-export from `app/api/push/route.ts`.\n\n### Service Worker helpers\n\nSee `@piro0919/next-push/sw`. `registerAll({ vapidPublicKey })` wires up `push`, `notificationclick`, `notificationclose`, and `pushsubscriptionchange`.\n\n## Recipes\n\n### Prisma\n\n```prisma\nmodel PushSubscription {\n  endpoint  String   @id @unique\n  p256dh    String\n  auth      String\n  userId    String?\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n}\n```\n\n```ts\n// app/api/push/route.ts\nimport { createPushHandler } from \"@piro0919/next-push/server\";\nimport { prisma } from \"@/lib/prisma\";\n\nexport const { POST, DELETE } = createPushHandler({\n  onSubscribe: async (sub) =\u003e {\n    await prisma.pushSubscription.upsert({\n      where: { endpoint: sub.endpoint },\n      create: { endpoint: sub.endpoint, p256dh: sub.keys.p256dh, auth: sub.keys.auth },\n      update: { p256dh: sub.keys.p256dh, auth: sub.keys.auth },\n    });\n  },\n  onUnsubscribe: async (endpoint) =\u003e {\n    await prisma.pushSubscription.delete({ where: { endpoint } }).catch(() =\u003e {});\n  },\n});\n```\n\n### Drizzle\n\n```ts\nexport const pushSubscriptions = sqliteTable(\"push_subscriptions\", {\n  endpoint: text(\"endpoint\").primaryKey(),\n  p256dh: text(\"p256dh\").notNull(),\n  auth: text(\"auth\").notNull(),\n  userId: text(\"user_id\"),\n});\n```\n\n### iOS: combine with use-pwa\n\niOS Safari only delivers push notifications when the site is installed as a PWA. Use [use-pwa](https://github.com/piro0919/use-pwa) to detect and prompt installation:\n\n```tsx\n\"use client\";\nimport { usePwa } from \"use-pwa\";\nimport { usePush } from \"@piro0919/next-push\";\n\nexport function NotifyButton() {\n  const { isPwa } = usePwa();\n  const { subscribe, isSupported } = usePush();\n  if (!isPwa) return \u003cp\u003eInstall this app to enable notifications on iOS.\u003c/p\u003e;\n  if (!isSupported) return null;\n  return \u003cbutton onClick={subscribe}\u003eEnable notifications\u003c/button\u003e;\n}\n```\n\n### Rich notification UI (icons, badges, actions)\n\nSend payloads can carry full `Notification` options:\n\n```ts\nawait sendPush(subscription, {\n  title: \"New message from Alice\",\n  body: \"Hi! When are you free?\",\n  icon: \"/icons/icon-192.png\",        // Main notification icon (shown next to the title)\n  badge: \"/icons/badge-72.png\",       // Monochrome icon for Android status bar\n  image: \"/preview/message.jpg\",      // Large preview image (Chrome Android only)\n  tag: \"chat-123\",                    // Replaces any notification with the same tag\n  url: \"/chat/123\",                   // Where to go when the notification is clicked\n  actions: [\n    { action: \"reply\", title: \"Reply\", icon: \"/icons/reply.png\" },\n    { action: \"mark-read\", title: \"Mark as read\" },\n  ],\n  data: { messageId: 456, userId: \"alice\" },\n});\n```\n\n### Default icon / badge at the SW level\n\nIf you don't want every sender to repeat the same icon, set defaults at SW registration:\n\n```ts\n// src/app/sw.ts (or public/sw.js — use --default-icon with the CLI)\nimport { registerAll } from \"@piro0919/next-push/sw\";\n\nregisterAll({\n  vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,\n  defaultNotification: {\n    icon: \"/icons/icon-192.png\",\n    badge: \"/icons/badge-72.png\",\n  },\n});\n```\n\nThe CLI can inline defaults into the generated `public/sw.js`:\n\n```bash\nnpx next-push init --default-icon /icons/icon-192.png --default-badge /icons/badge-72.png\n```\n\n### Batch sending\n\nSend the same payload to many subscriptions with bounded concurrency:\n\n```ts\nimport { sendPushBatch } from \"@piro0919/next-push/server\";\nimport { prisma } from \"@/lib/prisma\";\n\nconst subs = await prisma.pushSubscription.findMany();\nconst result = await sendPushBatch(subs, {\n  title: \"Daily digest\",\n  body: \"You have 3 new messages.\",\n}, {\n  concurrency: 20,\n  onProgress: (done, total) =\u003e console.log(`${done}/${total}`),\n});\n\n// Prune dead subscriptions\nawait prisma.pushSubscription.deleteMany({\n  where: { endpoint: { in: result.goneEndpoints } },\n});\n\nconsole.log(`${result.sent}/${result.total} delivered, ${result.failed} failures`);\n```\n\n### Handling retryable failures\n\n`sendPush` flags transient failures so you can retry with backoff:\n\n```ts\nconst result = await sendPush(subscription, payload);\n\nif (result.ok) return; // delivered\nif (result.gone) {\n  await db.subscription.delete({ where: { endpoint: subscription.endpoint } });\n  return;\n}\nif (result.retryable) {\n  const delay = (result.retryAfter ?? 60) * 1000;\n  setTimeout(() =\u003e sendPush(subscription, payload), delay);\n  return;\n}\n// Permanent failure — log and investigate\nconsole.error(\"Push failed permanently\", result.statusCode, result.error);\n```\n\n## Supported environments\n\n| | |\n|---|---|\n| Next.js | 15+ (App Router only) |\n| React   | 18+ |\n| Node.js | 20+ |\n| Chrome / Edge / Firefox (desktop + Android) | Latest 2 versions |\n| Safari macOS | 16+ |\n| **Safari iOS** | **16.4+ and installed as a PWA only** |\n| iOS Chrome / Firefox / Edge | ❌ Not supported (all use WebKit + PWA restriction) |\n| In-app browsers (LINE, Twitter, etc.) | ❌ Not supported |\n\n## Roadmap\n\n- v0.2: batched sending, Playwright E2E, richer A2HS recipes, Electron\n- v1.0: stable API, semver\n\n## License\n\nMIT © piro0919\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpiro0919%2Fnext-push","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpiro0919%2Fnext-push","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpiro0919%2Fnext-push/lists"}