<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LaraPush</title>
    <description>The latest articles on DEV Community by LaraPush (@larapush).</description>
    <link>https://dev.to/larapush</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4216040%2F66ae4ab3-eb4f-48b6-b760-7d6ff3b52f8a.png</url>
      <title>DEV Community: LaraPush</title>
      <link>https://dev.to/larapush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/larapush"/>
    <language>en</language>
    <item>
      <title>How Web Push Actually Works: Service Workers, VAPID and FCM in Plain Terms</title>
      <dc:creator>LaraPush</dc:creator>
      <pubDate>Thu, 29 Oct 2026 20:39:37 +0000</pubDate>
      <link>https://dev.to/larapush/how-web-push-actually-works-service-workers-vapid-and-fcm-in-plain-terms-3i35</link>
      <guid>https://dev.to/larapush/how-web-push-actually-works-service-workers-vapid-and-fcm-in-plain-terms-3i35</guid>
      <description>&lt;p&gt;Web push looks simple from the outside. A visitor clicks "Allow", and later a notification appears even though your site is closed. Underneath, there are four moving parts, and once you understand how they fit together, most push bugs become easy to diagnose.&lt;/p&gt;

&lt;p&gt;This is a developer-friendly walkthrough of the whole flow, with no vendor SDK in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four parts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your site&lt;/strong&gt;, which asks for permission and creates the subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service worker&lt;/strong&gt;, a script the browser keeps registered for your origin that can wake up when a message arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A push service&lt;/strong&gt;, run by the browser vendor. Chrome and most Chromium browsers use Firebase Cloud Messaging; Firefox uses Mozilla's autopush; Safari uses Apple's service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your application server&lt;/strong&gt;, which stores subscriptions and sends messages to the push service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API" rel="noopener noreferrer"&gt;Push API reference&lt;/a&gt; is the best single page for the browser side of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: register a service worker
&lt;/h2&gt;

&lt;p&gt;Everything starts with a service worker served from your own domain, usually at the root so its scope covers the whole site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the worker sits on a different origin, or in a subfolder with a narrow scope, subscriptions will fail or only work on some pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: ask permission at the right moment
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;Notification.requestPermission()&lt;/code&gt; on page load is the fastest way to get blocked. Chrome now shows quieter prompts for sites with low accept rates. Ask after a user gesture, ideally after a short in-page explanation of what they will receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: subscribe with your VAPID public key
&lt;/h2&gt;

&lt;p&gt;VAPID is a key pair that identifies your application server to the push service. You generate it once and keep the private key secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userVisibleOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;applicationServerKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VAPID_PUBLIC_KEY&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned subscription contains an &lt;code&gt;endpoint&lt;/code&gt; URL (which points at FCM, autopush or Apple) and two keys, &lt;code&gt;p256dh&lt;/code&gt; and &lt;code&gt;auth&lt;/code&gt;. Send this JSON to your server and store it. Those three values plus your private VAPID key are everything you need to reach that browser later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: send from your server
&lt;/h2&gt;

&lt;p&gt;To send, your server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;encrypts the payload with the subscriber's &lt;code&gt;p256dh&lt;/code&gt; and &lt;code&gt;auth&lt;/code&gt; keys (aes128gcm content encoding),&lt;/li&gt;
&lt;li&gt;signs a short JWT with the VAPID private key,&lt;/li&gt;
&lt;li&gt;makes an HTTP POST to the subscription's endpoint with a &lt;code&gt;TTL&lt;/code&gt; header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need a Firebase project for standard Web Push with VAPID; Chrome's endpoint on &lt;code&gt;fcm.googleapis.com&lt;/code&gt; accepts VAPID-signed requests directly. If you use the Firebase JavaScript SDK instead, the setup differs slightly, and Google's guide to &lt;a href="https://firebase.google.com/docs/cloud-messaging/js/client" rel="noopener noreferrer"&gt;setting up a JavaScript FCM client&lt;/a&gt; covers that path, including the move to the HTTP v1 API after the legacy send API was retired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: show the notification in the worker
&lt;/h2&gt;

&lt;p&gt;When the push service delivers the message, the browser wakes your service worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notificationclick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always show a notification for every push. With &lt;code&gt;userVisibleOnly: true&lt;/code&gt;, browsers may revoke permission from sites that receive pushes silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the responses
&lt;/h2&gt;

&lt;p&gt;Your server's response codes tell you a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;201 Created&lt;/strong&gt;: accepted for delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 or 410&lt;/strong&gt;: the subscription is gone. Delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;413&lt;/strong&gt;: payload too large; keep it under about 4 KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429&lt;/strong&gt;: you are being rate-limited; back off and retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most "push stopped working" tickets come down to never pruning 410s, a moved service worker, or a lost VAPID private key. The last one is unrecoverable, so back it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it or install it
&lt;/h2&gt;

&lt;p&gt;Writing this flow for one site is a weekend project. Running it reliably for many sites, with segmentation, scheduling, queues and a WordPress integration, is a lot more. If you want the control of owning your keys without writing the plumbing, LaraPush packages this exact flow as a panel you &lt;a href="https://larapush.com/features/" rel="noopener noreferrer"&gt;install on your own server&lt;/a&gt;, with web push sent via FCM and the keys staying on your VPS. Either way, knowing the moving parts makes you far better at debugging whichever setup you run.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>firebase</category>
      <category>notifications</category>
    </item>
  </channel>
</rss>
