<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="/feed.xsl"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Web-Push on jvalol</title><link>https://jva.lol/tags/web-push/</link><description>Recent content in Web-Push on jvalol</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 17 Jul 2026 21:45:00 -0600</lastBuildDate><atom:link href="https://jva.lol/tags/web-push/index.xml" rel="self" type="application/rss+xml"/><item><title>The notification that gave up after a minute</title><link>https://jva.lol/weblog/the-notification-that-gave-up-after-a-minute/</link><pubDate>Fri, 17 Jul 2026 21:45:00 -0600</pubDate><guid>https://jva.lol/weblog/the-notification-that-gave-up-after-a-minute/</guid><description>In which four healthy-looking layers hide two quiet failures</description><content:encoded>&lt;p&gt;PopsicleBoat grew browser push notifications recently. The pitch is simple: hear when someone answers you, without handing anyone an email address. Your browser asks permission, a subscription goes to the server, and nothing personal leaves the device. I shipped it, watched the push service accept my first delivery with a tidy &lt;code&gt;201 Created&lt;/code&gt;, and felt good about the whole thing.&lt;/p&gt;
&lt;p&gt;Then I replied to one of my own posts from my phone, looked at my laptop, and saw nothing.&lt;/p&gt;
&lt;p&gt;No banner. No chime. The reply was on the site, the inbox badge lit up, the notification email arrived — every channel working except the one I&amp;rsquo;d just built. And here&amp;rsquo;s what made it a good puzzle: every layer of the push stack, inspected on its own, reported success.&lt;/p&gt;
&lt;p&gt;The server sent the notification. The push service accepted it. The subscription was in the database. The service worker was registered. Four green lights, zero banners.&lt;/p&gt;
&lt;p&gt;Web push is a relay race with four runners: your server signs a message and hands it to a push service (Google&amp;rsquo;s, for Chromium browsers), the push service holds it for the browser, the browser wakes a service worker, and the service worker asks the operating system to draw a banner. A dropped baton anywhere shows up the same way — silence at the finish line — and no runner files a report.&lt;/p&gt;
&lt;p&gt;The first dropped baton was mine to find in the browser console. &lt;code&gt;Notification.permission&lt;/code&gt; said &lt;code&gt;&amp;quot;default&amp;quot;&lt;/code&gt;. Not &lt;code&gt;&amp;quot;granted&amp;quot;&lt;/code&gt;, not &lt;code&gt;&amp;quot;denied&amp;quot;&lt;/code&gt; — &lt;em&gt;never asked&lt;/em&gt;. Somewhere between enabling notifications and testing them, the site&amp;rsquo;s permission had ended up back at square one — a site-data sweep, probably — while the subscription it had authorized lived on in my database, perfectly valid, pointing at a browser that would no longer show anything. In that state, a page calling &lt;code&gt;new Notification()&lt;/code&gt; doesn&amp;rsquo;t error. It does nothing, silently, which is a bold choice for an API whose entire job is being noticed.&lt;/p&gt;
&lt;p&gt;One re-grant later (&amp;ldquo;forever,&amp;rdquo; this time), banners worked. Victory lasted about an hour, until I noticed pushes still vanished whenever the browser wasn&amp;rsquo;t running.&lt;/p&gt;
&lt;p&gt;That was the second baton, and it was hiding in a default I&amp;rsquo;d never questioned. Push services will happily hold a message for a browser that&amp;rsquo;s closed — that&amp;rsquo;s the whole point of the relay — but only as long as the message&amp;rsquo;s &lt;em&gt;time-to-live&lt;/em&gt; allows. The library I use sets that TTL to sixty seconds unless told otherwise. Sixty seconds. Close your laptop, get a reply two minutes later, and the push service shrugs and discards it. The &lt;code&gt;201&lt;/code&gt; it returned was entirely honest: message accepted. Nobody promised &lt;em&gt;kept&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The fix is one option:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-elixir" data-lang="elixir"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;send_notification(payload, message, &lt;span style="color:#e6db74"&gt;ttl&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;86_400&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A day. That&amp;rsquo;s roughly how long &amp;ldquo;someone answered you&amp;rdquo; stays worth interrupting someone for; any older and it&amp;rsquo;s the inbox&amp;rsquo;s job, which never forgets.&lt;/p&gt;
&lt;p&gt;Two lessons sailed home with this one. First: in a layered system where every layer answers &amp;ldquo;OK,&amp;rdquo; the bug lives in the gaps between the layers — the permission that reverted between grant and use, the message that expired between accepted and delivered. Debugging meant walking the relay in order and asking each runner not &amp;ldquo;did you succeed?&amp;rdquo; but &amp;ldquo;what did you hand the next runner, and did anyone catch it?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Second: defaults are decisions someone else made about your product. A conservative browser permission model, a sixty-second TTL — both defensible choices, by people who&amp;rsquo;d never seen my notification settings page and its promise to &lt;em&gt;hear when someone answers you&lt;/em&gt;. Keeping that promise meant finding every default standing between the reply and the banner, and overruling the ones that disagreed with it.&lt;/p&gt;
&lt;p&gt;The banners arrive now. Even the morning after.&lt;/p&gt;</content:encoded></item></channel></rss>