{"_id":"@am25/webpush","_rev":"4-f31b1d60e2671aac130b5ee76231f7cf","name":"@am25/webpush","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@am25/webpush","version":"1.0.0","keywords":["web-push","push-notifications","webpush","vapid","express","nodejs","typescript","self-hosted","am25"],"author":{"url":"https://alemartir.com","name":"Alejandro Mártir"},"license":"MIT","_id":"@am25/webpush@1.0.0","maintainers":[{"name":"itsmrtr","email":"hola@alemartir.com"}],"homepage":"https://github.com/am25-labs/webpush#readme","bugs":{"url":"https://github.com/am25-labs/webpush/issues"},"bin":{"webpush":"dist/bin/webpush.js","create-webpush":"dist/bin/create.js"},"dist":{"shasum":"3d99f8ec694edd040121af8191e2a1b6588e4b10","tarball":"https://registry.npmjs.org/@am25/webpush/-/webpush-1.0.0.tgz","fileCount":27,"integrity":"sha512-zHJETYAiLnOil+PLYPRDq28e/nmVKw5E6cwBypdF7SCvRePztgQY9xAUgBKUOnwG8KJv8HknpXTiX4Kb/VUbRw==","signatures":[{"sig":"MEYCIQDNulm+CtoTIQUYLOZbggwbXD3TQg96FrnWJVo/Wqv99wIhAMsdA3jHzf4JdxAsqaGgreFtk5AxeXORGhmUXUg0yATZ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36933},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"adb3bc7b6b5a1356f13366d4aa5d7a6b20214d50","scripts":{"dev":"tsc --watch","build":"tsc","start":"webpush","generate:vapid":"node dist/src/vapid.js","prepublishOnly":"pnpm build"},"_npmUser":{"name":"itsmrtr","email":"hola@alemartir.com"},"repository":{"url":"git+https://github.com/am25-labs/webpush.git","type":"git"},"_npmVersion":"10.9.2","description":"Self-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"cors":"2.8.6","dotenv":"17.3.1","express":"5.2.1","web-push":"3.6.7"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319","devDependencies":{"typescript":"^5.9.3","@types/cors":"^2.8.19","@types/node":"^25.5.0","@types/express":"^5.0.6","@types/web-push":"^3.6.4"},"_npmOperationalInternal":{"tmp":"tmp/webpush_1.0.0_1775111370938_0.9891741351449126","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@am25/webpush","version":"1.0.1","keywords":["web-push","push-notifications","webpush","vapid","express","nodejs","typescript","self-hosted","am25"],"author":{"url":"https://alemartir.com","name":"Alejandro Mártir"},"license":"MIT","_id":"@am25/webpush@1.0.1","maintainers":[{"name":"itsmrtr","email":"hola@alemartir.com"}],"homepage":"https://github.com/am25-labs/webpush#readme","bugs":{"url":"https://github.com/am25-labs/webpush/issues"},"bin":{"webpush":"dist/bin/webpush.js","create-webpush":"dist/bin/create.js"},"dist":{"shasum":"6e454c4cda5fe72bb3175fce0d7f8145be8b791e","tarball":"https://registry.npmjs.org/@am25/webpush/-/webpush-1.0.1.tgz","fileCount":27,"integrity":"sha512-2/9igSJ2ufNPT6Z5n9qsY8j1Ufg6iTKZt+2uvA99YMOC++HK3NjgVHlV2stamzuC/6VVANvLVFN57vQapR+tPg==","signatures":[{"sig":"MEUCIAgMo5POlfQmKZhEdM5d+qW84uKpRCBXgaefAoVQQ17xAiEArPln9sXEJE6Ro2Vkhu5gU7n3YYCjEnsVPci26d1u++0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37908},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"c89ba6a18c5c6bfba740c9ed72c110e508ac2c44","scripts":{"dev":"tsc --watch","build":"tsc","start":"webpush","generate:vapid":"node dist/src/vapid.js","prepublishOnly":"pnpm build"},"_npmUser":{"name":"itsmrtr","email":"hola@alemartir.com"},"repository":{"url":"git+https://github.com/am25-labs/webpush.git","type":"git"},"_npmVersion":"10.9.2","description":"Self-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"cors":"2.8.6","dotenv":"17.3.1","express":"5.2.1","web-push":"3.6.7"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319","devDependencies":{"typescript":"^5.9.3","@types/cors":"^2.8.19","@types/node":"^25.5.0","@types/express":"^5.0.6","@types/web-push":"^3.6.4"},"_npmOperationalInternal":{"tmp":"tmp/webpush_1.0.1_1775112547449_0.5749191622023047","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@am25/webpush","version":"1.0.2","keywords":["web-push","push-notifications","webpush","vapid","express","nodejs","typescript","self-hosted","am25"],"author":{"url":"https://alemartir.com","name":"Alejandro Mártir"},"license":"MIT","_id":"@am25/webpush@1.0.2","maintainers":[{"name":"itsmrtr","email":"hola@alemartir.com"}],"homepage":"https://github.com/am25-labs/webpush#readme","bugs":{"url":"https://github.com/am25-labs/webpush/issues"},"bin":{"webpush":"dist/bin/webpush.js","create-webpush":"dist/bin/create.js"},"dist":{"shasum":"8d4d2ab18a6206cd4d3dad94843aa7309778b1e7","tarball":"https://registry.npmjs.org/@am25/webpush/-/webpush-1.0.2.tgz","fileCount":27,"integrity":"sha512-nvEk+yIZHhCJmkW3JjZklfGKyjGKSI6XhlfxawCTVk9Luttwc5hHxNRvreT1BpVKgoEES3WVjtAj5sc8LbqTYg==","signatures":[{"sig":"MEUCIQC/WllqFQP2iHSjSNDp3ELDH3+pYsJeGyYY/hE28LvBUgIgd5Mjc2ULE7T3lceLoPpQcehQ2oc1S5UIcwmDb/sMjSg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":38135},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b5060478c0be721c5efab2f51870b7d6a60a4b9a","scripts":{"dev":"tsc --watch","build":"tsc","start":"webpush","generate:vapid":"node dist/src/vapid.js","prepublishOnly":"pnpm build"},"_npmUser":{"name":"itsmrtr","email":"hola@alemartir.com"},"repository":{"url":"git+https://github.com/am25-labs/webpush.git","type":"git"},"_npmVersion":"10.9.2","description":"Self-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"cors":"2.8.6","dotenv":"17.3.1","express":"5.2.1","web-push":"3.6.7"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319","devDependencies":{"typescript":"^5.9.3","@types/cors":"^2.8.19","@types/node":"^25.5.0","@types/express":"^5.0.6","@types/web-push":"^3.6.4"},"_npmOperationalInternal":{"tmp":"tmp/webpush_1.0.2_1775112785031_0.4439686618221683","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@am25/webpush","version":"1.1.0","description":"Self-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"bin":{"webpush":"dist/bin/webpush.js","create-webpush":"dist/bin/create.js"},"author":{"name":"Alejandro Mártir","url":"https://alemartir.com"},"homepage":"https://github.com/am25-labs/webpush#readme","repository":{"type":"git","url":"git+https://github.com/am25-labs/webpush.git"},"bugs":{"url":"https://github.com/am25-labs/webpush/issues"},"engines":{"node":">=18"},"keywords":["web-push","push-notifications","webpush","vapid","express","nodejs","typescript","self-hosted","am25"],"license":"MIT","dependencies":{"cors":"2.8.6","dotenv":"17.3.1","express":"5.2.1","web-push":"3.6.7"},"devDependencies":{"@types/cors":"^2.8.19","@types/express":"^5.0.6","@types/node":"^25.5.0","@types/web-push":"^3.6.4","typescript":"^5.9.3"},"scripts":{"build":"tsc","start":"node ./dist/src/server.js","dev":"tsc --watch","generate:vapid":"node dist/src/vapid.js"},"_id":"@am25/webpush@1.1.0","_integrity":"sha512-q7mMNRvCdR1U3JKEkqmDjpvg8AdZkra2tcF8GDwalVWv2J5OSWAGDuJmjbZauzYpn6of0N54Ir+/sPjqC6gc7w==","_resolved":"/tmp/ab65fd683c194f1e3d299044751ad6e7/am25-webpush-1.1.0.tgz","_from":"file:am25-webpush-1.1.0.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-q7mMNRvCdR1U3JKEkqmDjpvg8AdZkra2tcF8GDwalVWv2J5OSWAGDuJmjbZauzYpn6of0N54Ir+/sPjqC6gc7w==","shasum":"7fd4c890eac3ebb6dd73940e139f9a895054a7fc","tarball":"https://registry.npmjs.org/@am25/webpush/-/webpush-1.1.0.tgz","fileCount":27,"unpackedSize":39634,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@am25%2fwebpush@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCXwltmTViFm0CBwydUFchVRM7te81AicC4A4rPBb31gwIgaRnDwMLrRljCZwuRzOYCUtv5UpMi+q3D+kXVXBw97cE="}]},"_npmUser":{"name":"itsmrtr","email":"hola@alemartir.com"},"directories":{},"maintainers":[{"name":"itsmrtr","email":"hola@alemartir.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/webpush_1.1.0_1778782732502_0.9156854338380069"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-02T06:29:30.832Z","modified":"2026-05-14T18:18:53.007Z","1.0.0":"2026-04-02T06:29:31.074Z","1.0.1":"2026-04-02T06:49:07.588Z","1.0.2":"2026-04-02T06:53:05.171Z","1.1.0":"2026-05-14T18:18:52.669Z"},"bugs":{"url":"https://github.com/am25-labs/webpush/issues"},"author":{"name":"Alejandro Mártir","url":"https://alemartir.com"},"license":"MIT","homepage":"https://github.com/am25-labs/webpush#readme","keywords":["web-push","push-notifications","webpush","vapid","express","nodejs","typescript","self-hosted","am25"],"repository":{"type":"git","url":"git+https://github.com/am25-labs/webpush.git"},"description":"Self-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.","maintainers":[{"name":"itsmrtr","email":"hola@alemartir.com"}],"readme":"# @am25/webpush\n\nSelf-hosted Web Push notification server. Deploy in seconds and send push notifications from any application.\n\n```\nYour App  →  HTTP  →  @am25/webpush  →  Push Service  →  Browser\n```\n\nThe server **does not store subscriptions**. Your application is responsible for storing them.\n\n---\n\n## Two ways to use it\n\nThis project supports two different modes:\n\n### 1. As a published npm package\n\nThis is the main product experience.\n\nUse it when you want a standalone push server without cloning this repository:\n\n```bash\npnpm dlx @am25/webpush create-webpush\n```\n\nThat setup wizard creates a new app, installs `@am25/webpush`, writes the `.env`, and runs the server through the published CLI binary:\n\n```json\n{\n  \"scripts\": {\n    \"start\": \"webpush\"\n  }\n}\n```\n\nIn this mode, `webpush` comes from `node_modules/.bin`.\n\n### 2. From this Git repository\n\nUse it when you want to self-host from source, deploy directly from GitHub, or contribute to the project.\n\nIn this mode you build the TypeScript source and start the compiled server:\n\n```bash\npnpm install\npnpm build\npnpm start\n```\n\nFor deployment platforms such as Dokploy, the important distinction is:\n\n- Deploying the npm package means installing `@am25/webpush` into another app.\n- Deploying this repository means cloning source code and running the compiled output from `dist/`.\n\nIf you are deploying from GitHub, see [SELF-HOSTING.md](SELF-HOSTING.md).\n\n---\n\n## Setup\n\nRun the setup wizard. It generates your VAPID keys and API key automatically, then installs and starts the server.\n\n```bash\n# pnpm\npnpm dlx @am25/webpush create-webpush\n\n# npm\nnpx @am25/webpush create-webpush\n\n# yarn\nyarn dlx @am25/webpush create-webpush\n```\n\nThe wizard will ask for a directory name, a `VAPID_SUBJECT` (a `mailto:` or `https:` URI that identifies you), and an optional port. Everything else is generated automatically.\n\nAt the end it prints your **VAPID public key** — you'll need it in your frontend to subscribe users.\n\n---\n\n## API\n\n### Authentication\n\nThe `/send` and `/send-many` endpoints require an API key in the `Authorization` header:\n\n```\nAuthorization: Bearer your-api-key\n```\n\nThe `/health` endpoint is always public. Missing or incorrect keys return `401`:\n\n```json\n{ \"error\": \"Unauthorized\" }\n```\n\n---\n\n### GET /health\n\n```json\n{ \"status\": \"ok\" }\n```\n\n---\n\n### POST /send\n\nSend a push notification to a single subscription.\n\n```json\n{\n  \"subscription\": {\n    \"endpoint\": \"https://push-service/...\",\n    \"keys\": {\n      \"p256dh\": \"BNx4a...\",\n      \"auth\": \"abc1...\"\n    }\n  },\n  \"payload\": {\n    \"title\": \"New episode\",\n    \"body\": \"Episode 42 has been uploaded\",\n    \"url\": \"/\"\n  }\n}\n```\n\nResponse:\n\n```json\n{ \"success\": true }\n```\n\n---\n\n### POST /send-many\n\nBroadcast the same notification to multiple subscriptions in parallel.\n\n```json\n{\n  \"subscriptions\": [\n    { \"endpoint\": \"...\", \"keys\": { \"p256dh\": \"...\", \"auth\": \"...\" } },\n    { \"endpoint\": \"...\", \"keys\": { \"p256dh\": \"...\", \"auth\": \"...\" } }\n  ],\n  \"payload\": {\n    \"title\": \"Maintenance\",\n    \"body\": \"The server will restart at 3am\",\n    \"url\": \"/\"\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"success\": true,\n  \"total\": 2,\n  \"sent\": 2,\n  \"failed\": 0,\n  \"results\": [\n    { \"success\": true },\n    { \"success\": true }\n  ]\n}\n```\n\n---\n\n## Consuming from your app\n\nYour apps need these values from the server's `.env`:\n\n| Variable | Required | Where to use | What it does |\n|---|---|---|---|\n| `VAPID_SUBJECT` | Yes | Server only | Identifier used in VAPID claims. Must be a `mailto:` or `https:` URI. |\n| `VAPID_PUBLIC_KEY` | Yes | Server + frontend | Public key used by the server and exposed to the browser so it can create subscriptions. |\n| `VAPID_PRIVATE_KEY` | Yes | Server only | Private key used by the server to sign push requests. Never expose it to the client. |\n| `API_KEY` | Yes | Server + your backend | Bearer token that protects `/send` and `/send-many`. Your backend sends it when calling this push server. |\n| `PORT` | No | Server only | Port used by the HTTP server. Defaults to `5500`. |\n\nOnly `VAPID_PUBLIC_KEY` should reach the frontend.\n\n### TypeScript\n\n```ts\ninterface PushSubscription {\n  endpoint: string;\n  keys: { p256dh: string; auth: string };\n}\n\nasync function sendPush(subscription: PushSubscription) {\n  await fetch(`${process.env.PUSH_SERVER_URL}/send`, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${process.env.API_KEY}`,\n    },\n    body: JSON.stringify({\n      subscription,\n      payload: { title: \"Hello\", body: \"New message\", url: \"/\" },\n    }),\n  });\n}\n```\n\n### JavaScript\n\n```js\nawait fetch(`${process.env.PUSH_SERVER_URL}/send`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${process.env.API_KEY}`,\n  },\n  body: JSON.stringify({\n    subscription,\n    payload: { title: \"Hello\", body: \"New message\", url: \"/\" },\n  }),\n});\n```\n\n### Broadcast to multiple devices\n\n```ts\nconst subscriptions = await prisma.pushSubscription.findMany({ where: { userId } });\n\nawait fetch(`${process.env.PUSH_SERVER_URL}/send-many`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${process.env.API_KEY}`,\n  },\n  body: JSON.stringify({\n    subscriptions: subscriptions.map((s) => ({\n      endpoint: s.endpoint,\n      keys: s.keys as { p256dh: string; auth: string },\n    })),\n    payload: { title: \"Announcement\", body: \"Message for everyone\", url: \"/\" },\n  }),\n});\n```\n\n---\n\n## Subscription storage\n\nThe server does not persist subscriptions. Store them in your own database.\n\nA subscription object from the browser looks like:\n\n```json\n{\n  \"endpoint\": \"https://push-service/...\",\n  \"keys\": {\n    \"p256dh\": \"BNx4a...\",\n    \"auth\": \"abc1...\"\n  }\n}\n```\n\nExample Prisma model:\n\n```prisma\nmodel PushSubscription {\n  id        String   @id @default(cuid())\n  endpoint  String   @unique\n  keys      Json\n  createdAt DateTime @default(now())\n  userId    String\n\n  @@index([userId])\n}\n```\n\n---\n\n## Client setup\n\nTo receive push notifications the browser needs a **Service Worker**.\n\nCreate `sw.js` and serve it from the root of your site (e.g. `https://your-domain.com/sw.js`). If you already have a Service Worker, add the `push` and `notificationclick` listeners to it instead.\n\n| Framework | Location |\n|---|---|\n| Next.js | `public/sw.js` |\n| Nuxt | `public/sw.js` |\n| Astro | `public/sw.js` |\n| Vite / React SPA | `public/sw.js` |\n| Plain HTML | Root of your site |\n\n```js\nself.addEventListener(\"install\", () => self.skipWaiting());\n\nself.addEventListener(\"activate\", (event) => {\n  event.waitUntil(self.clients.claim());\n});\n\nself.addEventListener(\"push\", (event) => {\n  const data = event.data.json();\n  event.waitUntil(\n    self.registration.showNotification(data.title, {\n      body: data.body,\n      icon: data.icon || \"/icons/icon-192x192.png\",\n      data: { url: data.url || \"/\" },\n    })\n  );\n});\n\nself.addEventListener(\"notificationclick\", (event) => {\n  event.notification.close();\n  event.waitUntil(clients.openWindow(event.notification.data.url));\n});\n```\n\nRegister the Service Worker and subscribe the user:\n\n```js\nconst registration = await navigator.serviceWorker.register(\"/sw.js\");\nconst subscription = await registration.pushManager.subscribe({\n  userVisibleOnly: true,\n  applicationServerKey: VAPID_PUBLIC_KEY,\n});\n\n// Send `subscription` to your backend and store it\n```\n\n---\n\n## iOS (Safari) note\n\nOn **Android and desktop**, a Service Worker is all you need.\n\nOn **iOS Safari (16.4+)**, two additional requirements apply:\n\n1. The site must have a **web app manifest** (`manifest.json`).\n2. The user must **install the site on the Home Screen**.\n\nWithout both, iOS silently ignores push notifications.\n\nMinimal manifest:\n\n```json\n{\n  \"name\": \"Your App\",\n  \"short_name\": \"App\",\n  \"start_url\": \"/\",\n  \"display\": \"standalone\"\n}\n```\n\n```html\n<link rel=\"manifest\" href=\"/manifest.json\" />\n```\n\n---\n\n## Deployment\n\nRun the server behind a reverse proxy (Nginx, Traefik, Caddy, etc.) that handles SSL. Do not expose it directly to the internet.\n\n```\nInternet  →  Reverse Proxy (SSL)  →  @am25/webpush (:5500)\n              push.your-domain.com\n```\n\nYou'll need a domain or subdomain pointed to your server and an SSL certificate (Let's Encrypt works fine — most reverse proxies automate this).\n\nThe `VAPID_SUBJECT` in your `.env` should be a URI that identifies you (e.g. `mailto:you@example.com` or `https://your-domain.com`).\n\n---\n\n## Self-hosting from source\n\nFor advanced setups or to contribute, see [SELF-HOSTING.md](SELF-HOSTING.md).\n","readmeFilename":"README.md"}