<?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>Forem</title>
    <description>The most recent home feed on Forem.</description>
    <link>https://forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed"/>
    <language>en</language>
    <item>
      <title>How to Audit Your AI Agent Skills for Credential Exposure and Malicious Instructions</title>
      <dc:creator>Armor1</dc:creator>
      <pubDate>Fri, 15 May 2026 00:40:52 +0000</pubDate>
      <link>https://forem.com/armor1ai/how-to-audit-your-ai-agent-skills-for-credential-exposure-and-malicious-instructions-560</link>
      <guid>https://forem.com/armor1ai/how-to-audit-your-ai-agent-skills-for-credential-exposure-and-malicious-instructions-560</guid>
      <description>&lt;p&gt;Two independent security research groups published this week with findings that land on the same problem from different angles: AI agent skill files are a serious and underaudited supply chain surface, and the attack techniques targeting them are already in active use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scale Finding
&lt;/h2&gt;

&lt;p&gt;Capsule Security's analysis covered more than 200,000 agent skill files and 160,000 code files. The result that stands out: 2,909 of 19,618 distinct skill files carry hardcoded credentials alongside direct database write access. Roughly 15% of distinct skill files in active use. No additional exploit is required. Install the skill, the agent reads the skill configuration, the credentials are there.&lt;/p&gt;

&lt;p&gt;The same analysis found that AI workloads present a supply chain attack surface six times larger than traditional software. It also observed that malicious skills continue to persist and propagate after the campaigns that distributed them are officially terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Active Campaign
&lt;/h2&gt;

&lt;p&gt;A separate disclosure published the same week documents a March 2026 campaign targeting a popular AI coding agent framework. Attackers published deceptive community skills that appeared legitimate at a glance. The payload delivery mechanism was not a traditional malware dropper. It was the installation instruction inside the skill file itself.&lt;/p&gt;

&lt;p&gt;The skill's installation instructions directed the agent to perform operations that installed Remcos RAT and GhostLoader. The agent followed those instructions because that is exactly what installation instructions are for. No user interaction beyond installing the skill was required.&lt;/p&gt;

&lt;p&gt;This is a distinct campaign from the January 2026 supply chain attack covered in prior security reporting. Different delivery mechanism. Different payloads. The point of connection: both used the skill ecosystem as the distribution channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Attack Surface Looks Like
&lt;/h2&gt;

&lt;p&gt;An AI agent skill typically consists of a few components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A metadata file (often named &lt;code&gt;SKILL.md&lt;/code&gt; or similar) containing the skill's name, description, and installation instructions&lt;/li&gt;
&lt;li&gt;Configuration specifying what tools, permissions, and external resources the skill uses&lt;/li&gt;
&lt;li&gt;Optionally, code files the skill executes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The attack surface is broader than the code. The metadata file, particularly the installation instructions, is executed by the agent as part of skill setup. An agent that reads and follows installation instructions is following arbitrary instructions from whoever wrote that file. If the file was tampered with or written by a threat actor, those instructions are arbitrary commands.&lt;/p&gt;

&lt;p&gt;The credential exposure problem is a separate issue: skill files that embed API keys, database connection strings, or other credentials expose those values to every developer who installs the skill, to the agent that reads the configuration, and to anything else in the agent's context window.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Audit Your Skills
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Inventory what you have.&lt;/strong&gt; List every skill file currently active in your agent environment. For community-sourced skills, note the source and whether the version has changed since you installed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check skill metadata for credentials.&lt;/strong&gt; Search skill configuration files for patterns that suggest embedded credentials: connection strings, API key patterns, private key markers. A regex scan for common credential patterns across skill metadata is a reasonable first pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Review installation instructions for anomalies.&lt;/strong&gt; Read the installation instruction sections of skill files, particularly community-sourced ones. Installation instructions that invoke shell commands, download additional packages from unverified sources, or reference external URLs outside the skill's stated purpose are worth investigating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Check skill versions and provenance.&lt;/strong&gt; Skills that have changed since their last verified install are a flag. Skills from sources without a clear maintainer are a flag. If a skill you installed months ago now behaves differently, that is worth examining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Treat skill installs as supply chain events.&lt;/strong&gt; The same controls that apply to adding a dependency to package.json should apply to adding a skill to an agent environment. Review what it does, check the source, pin to a specific version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Armor1 Approaches This
&lt;/h2&gt;

&lt;p&gt;Armor1's skill security scanner evaluates every skill file before execution. The scanner checks for hardcoded credentials and credential misuse patterns, malicious installation instructions, data exfiltration patterns embedded in skill configuration, and supply chain risks such as references to unverified external packages or remote code in skill definitions. The scanner runs two passes: an initial analysis and a verification pass to reduce false positives.&lt;/p&gt;

&lt;p&gt;The credential exposure Capsule Security found at scale and the installation instruction attack vector documented in the March 2026 campaign both fall inside the categories the scanner evaluates.&lt;/p&gt;

&lt;p&gt;Check the risk of any MCP server in your environment with &lt;a href="https://mcp.armor1.ai/mcp-directory?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=ai-skill-supply-chain-2026-05&amp;amp;utm_content=devto-post" rel="noopener noreferrer"&gt;Armor1's free public catalog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To cover every agentic app, MCP, tool, skill, and plugin across your stack, sign up free &lt;a href="https://app.armor1.ai/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=ai-skill-supply-chain-2026-05&amp;amp;utm_content=devto-post" rel="noopener noreferrer"&gt;Here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>ai</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>How to Send Auth Codes via WhatsApp in Your App With Kinde</title>
      <dc:creator>Shola Jegede</dc:creator>
      <pubDate>Fri, 15 May 2026 00:38:16 +0000</pubDate>
      <link>https://forem.com/sholajegede/how-to-send-auth-codes-via-whatsapp-in-your-app-with-kinde-6k9</link>
      <guid>https://forem.com/sholajegede/how-to-send-auth-codes-via-whatsapp-in-your-app-with-kinde-6k9</guid>
      <description>&lt;p&gt;Your users are in San Francisco, Jakarta, São Paulo, and Sydney. They have WhatsApp open all day. They check it before they check their SMS inbox. When your app sends an OTP to their phone number, it lands in a carrier SMS thread they barely look at. When it lands in WhatsApp, they see it instantly.&lt;/p&gt;

&lt;p&gt;In this article, you will learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why WhatsApp OTP delivers better completion rates than SMS in high-penetration markets&lt;/li&gt;
&lt;li&gt;How Kinde delivers phone OTPs via WhatsApp when configured, with automatic SMS fallback&lt;/li&gt;
&lt;li&gt;What you need before you start: Twilio account, WhatsApp Sender, Kinde Pro&lt;/li&gt;
&lt;li&gt;How to configure Twilio Verify with a WhatsApp Sender&lt;/li&gt;
&lt;li&gt;How to connect your Twilio account to Kinde&lt;/li&gt;
&lt;li&gt;How to enable phone authentication in your Next.js app&lt;/li&gt;
&lt;li&gt;How to enable WhatsApp as the delivery channel in Kinde&lt;/li&gt;
&lt;li&gt;How to configure SMS as the automatic fallback&lt;/li&gt;
&lt;li&gt;How to test the full flow before going live&lt;/li&gt;
&lt;li&gt;What to watch for in Africa, South Asia, and Southeast Asia specifically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WhatsApp OTP Is the Right Choice for Your Market
&lt;/h2&gt;

&lt;p&gt;If your users are in Europe or North America, SMS OTP is a reasonable default. Delivery is reliable, carrier infrastructure is solid, and most users check texts promptly.&lt;/p&gt;

&lt;p&gt;If your users are in Nigeria, Kenya, Indonesia, Brazil, India, or the Philippines, the calculation is different. WhatsApp penetration in these markets is near-universal. Users across sub-Saharan Africa and Southeast Asia treat WhatsApp as their primary communication channel. SMS sits behind it, sometimes significantly behind.&lt;/p&gt;

&lt;p&gt;The data on this is clear. Twilio has documented that in heavy WhatsApp countries, 20 to 40 percent of users pick the WhatsApp channel when given the option for OTP delivery. More importantly, WhatsApp OTPs arrive over Wi-Fi connections. A user whose cellular data signal is weak but who is on Wi-Fi still receives the WhatsApp message instantly. SMS requires telephony connectivity. In markets where cellular data coverage is more reliable than SMS delivery, WhatsApp is not just more convenient. It is more reliable.&lt;/p&gt;

&lt;p&gt;There is also a cost argument. WhatsApp authentication messages cost 50 to 99 percent less than SMS in most markets globally. In developing markets where SMS costs are highest, the savings are largest. At any meaningful scale, the economics strongly favor WhatsApp.&lt;/p&gt;

&lt;p&gt;The correct production setup is WhatsApp first with automatic SMS fallback for the minority of users who do not have WhatsApp. That is exactly what Kinde gives you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frb16f82rql7whjv3c6m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frb16f82rql7whjv3c6m8.png" alt="Two-column comparison. LEFT: SMS OTP delivery flow showing Phone number → Carrier network → SMS inbox (variable delivery time, carrier dependent, fails on weak signal). RIGHT: WhatsApp OTP delivery flow showing Phone number → WhatsApp Business API → WhatsApp inbox (instant, Wi-Fi compatible, end-to-end encrypted, automatic SMS fallback if WhatsApp unavailable)" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Kinde Handles WhatsApp OTP
&lt;/h2&gt;

&lt;p&gt;Kinde supports phone number as both a primary authentication method and a secondary MFA factor. When phone authentication is configured, Kinde uses Twilio to deliver OTP codes. When you configure a WhatsApp Sender in your Twilio account and connect it to Kinde, Kinde automatically prefers WhatsApp for delivery and falls back to SMS when WhatsApp is unavailable.&lt;/p&gt;

&lt;p&gt;This is Twilio's Verify WhatsApp feature working through Kinde's phone authentication layer. You do not write any message-routing logic. Kinde and Twilio handle it. When a user enters their phone number on your sign-in screen, the OTP arrives in their WhatsApp. If they do not have WhatsApp or the delivery fails, Twilio's Verify API automatically retries via SMS.&lt;/p&gt;

&lt;p&gt;There are a few important constraints to know before you start:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kinde Pro is required for production phone authentication.&lt;/strong&gt; Kinde gives you 10 free SMS test sends per month on any plan to experiment with the feature, but production phone authentication (including WhatsApp) requires upgrading to the Pro plan or above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The OTP message format is not editable.&lt;/strong&gt; Kinde uses a standardized SMS/WhatsApp template that complies with OTP best practices. The message arrives as: 123456 is your verification code. followed by For your security, do not share this code. No app name, no URL, no hash. The sender name shown at the top of the WhatsApp chat comes from your registered Twilio business display name, not the message body. You cannot customize the message text or format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Twilio business account is required.&lt;/strong&gt; You cannot use a personal or trial Twilio account for production A2P (Application-to-Person) messaging. You need a Twilio business account with a configured phone number or Messaging Service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10DLC registration may be required in the US.&lt;/strong&gt; If your users are in the United States, Twilio requires 10DLC registration for SMS. Check Twilio's guidelines and A2P messaging requirements for your target countries before setting up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need Before You Start
&lt;/h2&gt;

&lt;p&gt;Before touching the Kinde dashboard, have the following ready:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Twilio business account&lt;/strong&gt; with your Account SID and Auth Token from the Twilio Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Twilio phone number or Messaging Service SID.&lt;/strong&gt; Twilio gives you two options for identifying the sender: a specific phone number or a Messaging Service (a pool of numbers Twilio manages for deliverability). The Messaging Service is recommended for production because Twilio handles number rotation and deliverability automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A WhatsApp Sender configured in Twilio.&lt;/strong&gt; This is a WhatsApp Business Account (WABA) phone number registered with Meta and approved for authentication message templates. Twilio's Verify WhatsApp documentation walks through this process. You need Meta Business verification and an approved authentication template before WhatsApp delivery works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Kinde account on the Pro plan or above.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Next.js app with the Kinde SDK installed.&lt;/strong&gt; If you are starting fresh, the Kinde Next.js quickstart at &lt;a href="https://docs.kinde.com" rel="noopener noreferrer"&gt;docs.kinde.com&lt;/a&gt; gets you to a working auth setup in under ten minutes.&lt;/p&gt;

&lt;p&gt;Note: You can complete the Kinde configuration and test with the 10 free SMS sends before completing the WhatsApp Sender setup in Twilio. This lets you verify the Kinde connection is working before adding WhatsApp to the mix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #1: Configure Twilio for Phone and WhatsApp Delivery
&lt;/h2&gt;

&lt;p&gt;Log in to your Twilio Console at &lt;a href="https://console.twilio.com" rel="noopener noreferrer"&gt;console.twilio.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get your credentials.&lt;/strong&gt; From the Console Dashboard, copy your Account SID and Auth Token. Keep the Auth Token private.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up a Messaging Service (recommended) or phone number.&lt;/strong&gt; If using a Messaging Service, navigate to &lt;strong&gt;Messaging → Services → Create Messaging Service&lt;/strong&gt;. Give it a name, add your sender number to it, and copy the Messaging Service SID.&lt;/p&gt;

&lt;p&gt;If using a specific Twilio phone number instead, navigate to &lt;strong&gt;Phone Numbers → Manage → Active numbers&lt;/strong&gt; and copy the phone number in E.164 format (e.g. &lt;code&gt;+12025551234&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the WhatsApp Sender for Verify.&lt;/strong&gt; This is the step that enables WhatsApp delivery. Navigate to &lt;strong&gt;Messaging → Senders → WhatsApp senders&lt;/strong&gt; in the Twilio Console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78y528rv9h693ly0nwi4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78y528rv9h693ly0nwi4.webp" alt="Twilio Console showing the Verify section in the left sidebar, with WhatsApp Senders highlighted" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Add Sender&lt;/strong&gt;. You will be prompted to connect a WhatsApp Business Account. This requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Meta Business Account (verified)&lt;/li&gt;
&lt;li&gt;A phone number registered as a WhatsApp Business number&lt;/li&gt;
&lt;li&gt;An approved WhatsApp authentication message template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twilio auto-creates the authentication template in supported languages once you complete the WhatsApp Sender setup. The template uses the Copy Code format, which displays the OTP with a button users tap to copy it to their clipboard.&lt;/p&gt;

&lt;p&gt;Note: WhatsApp Sender approval from Meta typically takes one to three business days. Plan your timeline accordingly. You can continue with the Kinde setup and test using SMS while waiting for WhatsApp approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #2: Connect Twilio to Kinde
&lt;/h2&gt;

&lt;p&gt;With Twilio credentials ready, connect them to Kinde.&lt;/p&gt;

&lt;p&gt;In your Kinde dashboard, navigate to &lt;strong&gt;Settings → Environment → Phone providers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F82l2xttxlh01kxc9fuqy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F82l2xttxlh01kxc9fuqy.png" alt="Kinde Settings showing the Phone providers section with a form to enter Twilio credentials. Fields visible: Account SID, Auth Token, and the choice between Messaging Service SID or Twilio Phone Number. Show the fields partially filled" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter your Twilio credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account SID&lt;/strong&gt;: from your Twilio Console dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Token&lt;/strong&gt;: from your Twilio Console dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging service SID or Twilio phone number&lt;/strong&gt;: enter whichever you set up in Step #1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to use a fallback in case the primary Twilio service is interrupted, enable the fallback service option and configure a secondary provider.&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Terrific! Kinde can now route phone OTP delivery through Twilio. The WhatsApp channel activates automatically once your WhatsApp Sender is approved in Twilio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #3: Enable Phone Authentication in Kinde
&lt;/h2&gt;

&lt;p&gt;With Twilio connected, turn on phone as an authentication method for your application.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Settings → Environment → Authentication&lt;/strong&gt;. In the &lt;strong&gt;Passwordless&lt;/strong&gt; section, find the &lt;strong&gt;Phone&lt;/strong&gt; tile and select &lt;strong&gt;Configure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ywuji2camdxln3odzu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ywuji2camdxln3odzu1.png" alt="Kinde Settings &amp;gt; Environment &amp;gt; Authentication page showing the Passwordless section with Email + code, Phone, and Username + code tiles. The Phone tile has a " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the configuration window, toggle phone authentication on for the applications you want it active for. If you have multiple applications in your Kinde environment, you can enable it selectively per application.&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your sign-in screen now includes a phone number input option alongside the existing email input. Users can choose to sign in with their phone number and receive an OTP instead of entering a password or email code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #4: Enable WhatsApp as the Delivery Channel
&lt;/h2&gt;

&lt;p&gt;With phone authentication active and Twilio connected, the final step is confirming that WhatsApp is set as the preferred delivery channel.&lt;/p&gt;

&lt;p&gt;In your Kinde dashboard, navigate to &lt;strong&gt;Settings → Environment → Phone providers&lt;/strong&gt;. In your Twilio configuration, you will see the WhatsApp delivery option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnitlv0mmfdu8wso5nne7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnitlv0mmfdu8wso5nne7.png" alt="Kinde Settings &amp;gt; Phone providers showing the Twilio configuration with a WhatsApp section. Show the toggle or setting that enables WhatsApp as the preferred delivery channel, with SMS shown as the fallback" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enable the WhatsApp delivery option. Once enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kinde sends the OTP via WhatsApp when the user's phone number is registered with WhatsApp&lt;/li&gt;
&lt;li&gt;If WhatsApp delivery fails (user does not have WhatsApp, or the delivery fails for any reason), Twilio's Verify API automatically falls back to SMS&lt;/li&gt;
&lt;li&gt;No code changes are needed in your application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fallback happens silently. Your users do not see an error or need to request a retry. Twilio detects the WhatsApp delivery failure and retries via SMS automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #5: Configure Phone Auth as MFA (Optional)
&lt;/h2&gt;

&lt;p&gt;If you want to use WhatsApp OTP as a second factor rather than (or in addition to) a primary authentication method, configure it at the MFA level.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Settings → Environment → Multi-factor auth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vdkmuzniccn2vr6q4mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4vdkmuzniccn2vr6q4mc.png" alt="Kinde Settings &amp;gt; Multi-factor auth page showing the " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Additional authentication methods&lt;/strong&gt; section, toggle on &lt;strong&gt;SMS&lt;/strong&gt;. Per the Kinde docs, the SMS MFA option delivers via WhatsApp when configured, with automatic fallback to SMS. The WhatsApp preference follows from your Twilio configuration in Step #4 automatically.&lt;/p&gt;

&lt;p&gt;Set MFA to &lt;strong&gt;Yes&lt;/strong&gt; (mandatory) or &lt;strong&gt;Optional&lt;/strong&gt; depending on your product's security requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes&lt;/strong&gt;: every user is required to set up MFA on first sign-in. For products targeting enterprise customers or handling sensitive data, this is the right default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional&lt;/strong&gt;: users are prompted to set up MFA but can skip it. They can enable it later from their profile settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: if your primary authentication method is already phone OTP (email OTP), do not set phone SMS as the secondary MFA factor. Kinde recommends against using the same channel for both primary and secondary factors. If your primary auth is email OTP, use phone SMS or authenticator app for MFA. If your primary auth is phone OTP, use email or authenticator app for MFA.&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #6: Wire Phone Authentication Into Your Next.js App
&lt;/h2&gt;

&lt;p&gt;Kinde's hosted sign-in screen already includes the phone number input once you enable phone authentication. Your Next.js app does not need code changes to show the phone sign-in option. It appears automatically on the Kinde-hosted auth pages.&lt;/p&gt;

&lt;p&gt;However, if you want to pre-fill the phone number for users coming from a known context (for example, users you have invited via phone number), use the &lt;code&gt;login_hint&lt;/code&gt; parameter to reduce friction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/[kindeAuth]/route.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Standard Kinde auth setup — no changes needed for basic WhatsApp OTP&lt;/span&gt;

&lt;span class="c1"&gt;// For login with pre-filled phone number hint:&lt;/span&gt;
&lt;span class="c1"&gt;// Pass login_hint in the authorization URL when you know the user's phone number&lt;/span&gt;
&lt;span class="c1"&gt;// This skips the phone number entry step&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handleAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@kinde-oss/kinde-auth-nextjs/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;handleAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For inviting users with a known phone number and pre-filling the sign-in form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/PhoneInviteButton.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// Pre-fills the phone number on the Kinde sign-in screen&lt;/span&gt;
&lt;span class="c1"&gt;// Useful when you know the user's phone number before they authenticate&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LoginLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@kinde-oss/kinde-auth-nextjs/components&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PhoneInviteButtonProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// E.164 format e.g. "+2348012345678"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PhoneInviteButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;PhoneInviteButtonProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginLink&lt;/span&gt;
      &lt;span class="na"&gt;authUrlParams&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// login_hint pre-fills the identity field on the Kinde sign-in screen&lt;/span&gt;
        &lt;span class="c1"&gt;// The user sees their phone number already entered and just confirms it&lt;/span&gt;
        &lt;span class="na"&gt;login_hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// connection_id routes directly to phone authentication&lt;/span&gt;
        &lt;span class="c1"&gt;// Skips the method selection screen&lt;/span&gt;
        &lt;span class="na"&gt;connection_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Sign in with phone
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;LoginLink&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;For protecting API routes on the server, the existing &lt;code&gt;getKindeServerSession&lt;/code&gt; approach works identically regardless of whether the user authenticated via email or phone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/example/route.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Phone-authenticated users produce the same session as email-authenticated users&lt;/span&gt;
&lt;span class="c1"&gt;// No changes needed to protected route logic&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getKindeServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@kinde-oss/kinde-auth-nextjs/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getKindeServerSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// user.phone is populated when the user authenticated via phone number&lt;/span&gt;
  &lt;span class="c1"&gt;// user.email is populated when the user authenticated via email&lt;/span&gt;
  &lt;span class="c1"&gt;// Both may be present if the user has linked both identities&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;email&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #7: Test the Full WhatsApp OTP Flow
&lt;/h2&gt;

&lt;p&gt;Before going live, test three scenarios in your Kinde non-production environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1: WhatsApp delivery to a WhatsApp-registered number&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter a phone number that is registered with WhatsApp. Kinde should route the OTP through Twilio Verify to WhatsApp. The code arrives in your WhatsApp with your Twilio-registered business name as the sender.&lt;/p&gt;

&lt;p&gt;Note: Kinde provides 10 free SMS sends per month for testing. These 10 free sends apply to SMS delivery. WhatsApp delivery tests use your Twilio Verify credits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 2: Automatic SMS fallback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To test the fallback, enter a valid phone number that is not registered with WhatsApp, or temporarily disable WhatsApp in your Twilio account. Twilio Verify should detect the WhatsApp delivery failure and automatically send via SMS. The OTP code is the same in both channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 3: MFA flow (if configured)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have set phone SMS as an MFA factor, sign in with email OTP first. On the second factor screen, Kinde prompts for the phone-based code. Enter your phone number if not already registered, and confirm the WhatsApp delivery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tb6z7iwbxobnm73v6z9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tb6z7iwbxobnm73v6z9.png" alt="Kinde-hosted sign-in screen showing the phone number input field. Show a clean phone number entry UI with the country code dropdown and the number field. This is what users see when they choose phone authentication" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64934qlrvd9t0v31j2fh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64934qlrvd9t0v31j2fh.webp" alt="A WhatsApp message on a phone showing a Kinde OTP delivery. The message shows the standard format" width="800" height="775"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Country-Specific Notes for African and Southeast Asian Markets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nigeria and Ghana:&lt;/strong&gt; WhatsApp penetration is extremely high, above 90 percent of smartphone users. SMS delivery is inconsistent in some areas. WhatsApp OTP is strongly recommended as the primary channel. Twilio's A2P messaging in Nigeria requires pre-registered sender IDs. Check Twilio's Nigeria SMS guidelines before launch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kenya, Tanzania, Uganda:&lt;/strong&gt; WhatsApp is widely used but M-Pesa and other mobile money services mean users are accustomed to SMS-based verification from financial services. WhatsApp OTP delivers a better experience but SMS fallback is particularly important in rural areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indonesia, Philippines, Vietnam:&lt;/strong&gt; Near-universal WhatsApp (and Line, in some markets) penetration. International SMS is expensive from most providers. WhatsApp OTP delivers significant cost savings at scale here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;India:&lt;/strong&gt; WhatsApp has over 500 million users. However, Meta has implemented messaging limits for WhatsApp Business in India. Review the current Meta Business Platform limits for Indian traffic before projecting volume. Twilio's documentation covers the current limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brazil:&lt;/strong&gt; One of the highest WhatsApp penetration rates globally. WhatsApp OTP is effectively the standard for authentication flows built for Brazilian users.&lt;/p&gt;

&lt;p&gt;For all markets: always test with real numbers in the target country before launch. Carrier behavior, number formatting requirements (always use E.164 format), and delivery timing vary by region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OTP not received via WhatsApp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check that your WhatsApp Sender is approved in Twilio and that the authentication template is active. An unapproved or suspended sender silently fails delivery. Check Twilio's delivery logs for error codes. Error code 63024 from Twilio means the phone number is not associated with a WhatsApp account and the fallback to SMS should have triggered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OTP received via SMS instead of WhatsApp&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the expected behavior when WhatsApp delivery is not available. If you expected WhatsApp delivery but got SMS, check: Is the destination phone number registered with WhatsApp? Is the WhatsApp Sender active and approved in Twilio? Has the user blocked your business sender in WhatsApp?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"You need to enter your Twilio account details" message in Kinde&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This appears when Kinde's phone authentication is configured but Twilio credentials have not been saved, or the credentials are invalid. Navigate to &lt;strong&gt;Settings → Environment → Phone providers&lt;/strong&gt; and re-enter your Twilio Account SID and Auth Token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users in the United States not receiving SMS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;US carriers require 10DLC registration for A2P SMS. If your Twilio account is not 10DLC registered for US traffic, messages to US numbers will fail. Complete 10DLC registration in Twilio before enabling phone authentication for US users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phone number formatting errors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kinde and Twilio both require phone numbers in E.164 format (e.g. &lt;code&gt;+2348012345678&lt;/code&gt; for a Nigerian number). If users enter numbers without country codes, the OTP send will fail. Kinde's hosted sign-in screen includes a country code dropdown to help users format numbers correctly. If you are pre-filling numbers via &lt;code&gt;login_hint&lt;/code&gt;, always format them in E.164.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here is a summary of the full setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twilio business account&lt;/td&gt;
&lt;td&gt;Get Account SID, Auth Token, Messaging Service&lt;/td&gt;
&lt;td&gt;Twilio Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp Sender&lt;/td&gt;
&lt;td&gt;Register WhatsApp Business number with Meta via Twilio&lt;/td&gt;
&lt;td&gt;Twilio Console &amp;gt; Verify &amp;gt; WhatsApp Senders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect Twilio to Kinde&lt;/td&gt;
&lt;td&gt;Enter Twilio credentials&lt;/td&gt;
&lt;td&gt;Kinde &amp;gt; Settings &amp;gt; Phone providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable phone auth&lt;/td&gt;
&lt;td&gt;Toggle on Phone in Passwordless section&lt;/td&gt;
&lt;td&gt;Kinde &amp;gt; Settings &amp;gt; Authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable WhatsApp delivery&lt;/td&gt;
&lt;td&gt;Configure WhatsApp as preferred channel&lt;/td&gt;
&lt;td&gt;Kinde &amp;gt; Settings &amp;gt; Phone providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure MFA (optional)&lt;/td&gt;
&lt;td&gt;Enable SMS as second factor&lt;/td&gt;
&lt;td&gt;Kinde &amp;gt; Settings &amp;gt; Multi-factor auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;3-scenario flow test before going live&lt;/td&gt;
&lt;td&gt;Non-production environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ev7wazliuvfvd9fxos3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ev7wazliuvfvd9fxos3.png" alt="Full delivery flow showing Your App (user enters phone number) → Kinde (routes to Twilio) → Twilio Verify (checks WhatsApp availability) → two paths: WhatsApp path (user has WhatsApp, message delivered via WhatsApp Business API, end-to-end encrypted) and SMS fallback path (user does not have WhatsApp or delivery fails, Twilio retries via SMS carrier network). Both paths return to user entering OTP in your app → Kinde verifies → user authenticatedn" width="800" height="1658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, you connected Twilio Verify to Kinde, registered a WhatsApp Business Sender, enabled phone authentication, and configured WhatsApp as the preferred OTP delivery channel with automatic SMS fallback. Users in markets where WhatsApp dominates now receive their authentication codes in the app they check first, over Wi-Fi when needed, at a fraction of the SMS cost.&lt;/p&gt;

&lt;p&gt;The setup is the same for phone as primary auth and as MFA. Once your Twilio credentials are in Kinde and your WhatsApp Sender is approved, the routing is handled entirely by Twilio Verify. Your application code does not change. Kinde issues the same JWT regardless of whether the OTP arrived via WhatsApp or SMS.&lt;/p&gt;

&lt;p&gt;Kinde is free for up to 10,500 monthly active users. Phone authentication and WhatsApp OTP require the Pro plan. Create your account at &lt;a href="https://kinde.com" rel="noopener noreferrer"&gt;kinde.com&lt;/a&gt; and meet your users where they already are.&lt;/p&gt;

</description>
      <category>kinde</category>
      <category>tutorial</category>
      <category>auth</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Desk Meter: Building a Local-First Runtime Dashboard Toward MuseMeter</title>
      <dc:creator>Gary Doman/TizWildin</dc:creator>
      <pubDate>Fri, 15 May 2026 00:36:36 +0000</pubDate>
      <link>https://forem.com/tizwildin/ai-desk-meter-building-a-local-first-runtime-dashboard-toward-musemeter-h8m</link>
      <guid>https://forem.com/tizwildin/ai-desk-meter-building-a-local-first-runtime-dashboard-toward-musemeter-h8m</guid>
      <description>&lt;h1&gt;
  
  
  AI Desk Meter: Building a Local-First Runtime Dashboard Toward MuseMeter
&lt;/h1&gt;

&lt;p&gt;I’m building &lt;strong&gt;AI Desk Meter&lt;/strong&gt;, an open-source local-first runtime dashboard for AI status, runtime state, and companion-style desktop visibility.&lt;/p&gt;

&lt;p&gt;The project is also the open-source foundation leading toward &lt;strong&gt;MuseMeter&lt;/strong&gt;, a future second-brain / Neural Synth / AI buddy product.&lt;/p&gt;

&lt;p&gt;The core idea is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local runtime state → JSON source of truth → dashboard sync → native/app/hardware display
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI Desk Meter is meant to stay lightweight, inspectable, and useful without requiring a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Desk Meter is
&lt;/h2&gt;

&lt;p&gt;AI Desk Meter is a local-first dashboard project that displays runtime state from a JSON-backed source of truth.&lt;/p&gt;

&lt;p&gt;It is designed as a visible companion surface for an AI/runtime system, showing state in a way that can eventually connect to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local AI agents&lt;/li&gt;
&lt;li&gt;runtime monitors&lt;/li&gt;
&lt;li&gt;desktop companion apps&lt;/li&gt;
&lt;li&gt;small hardware displays&lt;/li&gt;
&lt;li&gt;Raspberry Pi / ESP32 style companion builds&lt;/li&gt;
&lt;li&gt;native app shells&lt;/li&gt;
&lt;li&gt;future MuseMeter hardware/software releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current project is focused on making the foundation clean, open, and usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I’m building it
&lt;/h2&gt;

&lt;p&gt;Most AI interfaces are either chat boxes, dashboards, or cloud services.&lt;/p&gt;

&lt;p&gt;AI Desk Meter is aimed at a different interaction pattern: a small visible runtime companion that can sit on your desktop, show what the system is doing, and eventually become a bridge between AI status, local memory, agent state, and companion hardware.&lt;/p&gt;

&lt;p&gt;The long-term direction is &lt;strong&gt;MuseMeter&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;second-brain style companion&lt;/li&gt;
&lt;li&gt;local-first AI buddy&lt;/li&gt;
&lt;li&gt;Neural Synth-inspired visual interface&lt;/li&gt;
&lt;li&gt;desktop/runtime visibility&lt;/li&gt;
&lt;li&gt;optional companion hardware&lt;/li&gt;
&lt;li&gt;open-source foundation before the commercial 3.0 product line&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local-first design
&lt;/h2&gt;

&lt;p&gt;The project is intentionally built around a no-server default.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no required cloud backend&lt;/li&gt;
&lt;li&gt;dashboard state comes from local files/runtime output&lt;/li&gt;
&lt;li&gt;JSON can act as the source of truth&lt;/li&gt;
&lt;li&gt;the system can be inspected directly&lt;/li&gt;
&lt;li&gt;future native/hardware layers can read the same state model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the project simple, portable, and easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current release direction
&lt;/h2&gt;

&lt;p&gt;The current AI Desk Meter direction includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runtime dashboard sync&lt;/li&gt;
&lt;li&gt;JSON-backed state updates&lt;/li&gt;
&lt;li&gt;local-first/no-server architecture&lt;/li&gt;
&lt;li&gt;support/funding links&lt;/li&gt;
&lt;li&gt;open-source project foundation&lt;/li&gt;
&lt;li&gt;future native app holster direction&lt;/li&gt;
&lt;li&gt;future companion hardware direction&lt;/li&gt;
&lt;li&gt;roadmap toward MuseMeter 3.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The small pixel companion character and “Musing...” state are intentional.&lt;/p&gt;

&lt;p&gt;Right now, &lt;strong&gt;Musing...&lt;/strong&gt; represents an active response/action/loading state. Later versions may split this into more specific runtime states such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;responding&lt;/li&gt;
&lt;li&gt;loading&lt;/li&gt;
&lt;li&gt;thinking&lt;/li&gt;
&lt;li&gt;idle&lt;/li&gt;
&lt;li&gt;action running&lt;/li&gt;
&lt;li&gt;waiting for input&lt;/li&gt;
&lt;li&gt;agent task active&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why JSON as source of truth
&lt;/h2&gt;

&lt;p&gt;The dashboard is built around a JSON state model because it gives the project a clean bridge between layers.&lt;/p&gt;

&lt;p&gt;A JSON runtime state can be read by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the web dashboard&lt;/li&gt;
&lt;li&gt;a native app wrapper&lt;/li&gt;
&lt;li&gt;Python CLI tools&lt;/li&gt;
&lt;li&gt;hardware companion displays&lt;/li&gt;
&lt;li&gt;future agent runtimes&lt;/li&gt;
&lt;li&gt;test scripts&lt;/li&gt;
&lt;li&gt;docs and demos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the dashboard more than a static UI. It becomes a visible surface for a local runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MuseMeter fits
&lt;/h2&gt;

&lt;p&gt;AI Desk Meter is the open-source foundation.&lt;/p&gt;

&lt;p&gt;MuseMeter is the larger product direction.&lt;/p&gt;

&lt;p&gt;The plan is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Desk Meter open-source foundation
→ runtime dashboard stability
→ native app shell / holster
→ real Muse/agent connection
→ companion hardware support
→ MuseMeter 3.0 commercial package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything leading up to the commercial 3.0 direction is meant to preserve the open-source foundation while proving the runtime/dashboard concept in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/GareBear99/ai-desk-meter" rel="noopener noreferrer"&gt;https://github.com/GareBear99/ai-desk-meter&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’m looking for
&lt;/h2&gt;

&lt;p&gt;I’m looking for feedback from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI developers&lt;/li&gt;
&lt;li&gt;local-first app builders&lt;/li&gt;
&lt;li&gt;Python developers&lt;/li&gt;
&lt;li&gt;web dashboard developers&lt;/li&gt;
&lt;li&gt;hardware/display builders&lt;/li&gt;
&lt;li&gt;Raspberry Pi users&lt;/li&gt;
&lt;li&gt;ESP32/Arduino experimenters&lt;/li&gt;
&lt;li&gt;people interested in AI companion interfaces&lt;/li&gt;
&lt;li&gt;open-source maintainers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful feedback includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dashboard layout issues&lt;/li&gt;
&lt;li&gt;JSON runtime state suggestions&lt;/li&gt;
&lt;li&gt;native app packaging ideas&lt;/li&gt;
&lt;li&gt;hardware display ideas&lt;/li&gt;
&lt;li&gt;local-first architecture feedback&lt;/li&gt;
&lt;li&gt;UI/UX suggestions&lt;/li&gt;
&lt;li&gt;install/run issues&lt;/li&gt;
&lt;li&gt;roadmap feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Long-term vision
&lt;/h2&gt;

&lt;p&gt;The long-term goal is a small, useful, local-first AI companion surface that can grow from a web dashboard into a native app and eventually into hardware.&lt;/p&gt;

&lt;p&gt;AI Desk Meter is the foundation.&lt;/p&gt;

&lt;p&gt;MuseMeter is the product horizon.&lt;/p&gt;

&lt;p&gt;I’m building it in public so the runtime, dashboard, and companion architecture can be tested, improved, and documented as it grows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to fix native module errors when switching JavaScript runtimes</title>
      <dc:creator>Alan West</dc:creator>
      <pubDate>Fri, 15 May 2026 00:31:38 +0000</pubDate>
      <link>https://forem.com/alanwest/how-to-fix-native-module-errors-when-switching-javascript-runtimes-3460</link>
      <guid>https://forem.com/alanwest/how-to-fix-native-module-errors-when-switching-javascript-runtimes-3460</guid>
      <description>&lt;h2&gt;
  
  
  The error that ruins your Monday
&lt;/h2&gt;

&lt;p&gt;You spent the weekend migrating your build pipeline to a faster JavaScript runtime. Tests passed locally. CI was green. You shipped it. Then Monday morning, your monitoring lights up: &lt;code&gt;Error: The module 'XXX.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 108. This version requires NODE_MODULE_VERSION 115.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or worse — silent failures. Functions that worked yesterday now return undefined. A crypto library that always hashed correctly now segfaults on production-shaped inputs.&lt;/p&gt;

&lt;p&gt;I ran into this last month migrating a media-processing service off an older Node version. The lesson cost me a Sunday and most of my patience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why native modules are so fragile
&lt;/h2&gt;

&lt;p&gt;When you &lt;code&gt;require('sharp')&lt;/code&gt; or &lt;code&gt;require('better-sqlite3')&lt;/code&gt;, you're not just pulling in JavaScript. You're loading a compiled &lt;code&gt;.node&lt;/code&gt; binary that talks to the engine through one of three ABIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct V8 bindings&lt;/strong&gt; — raw C++ calls into V8 internals. Fast, brittle, version-locked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAN&lt;/strong&gt; (Native Abstractions for Node) — a header-only abstraction layer over V8 changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-API (N-API)&lt;/strong&gt; — a stable ABI that promises forward compatibility across Node major versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trouble: every JavaScript runtime advertises "Node.js compatibility," but compatibility means different things at different layers. Pure JavaScript code? Almost always fine. CommonJS resolution? Mostly fine. Native modules? It depends entirely on how the module was built — and whether the target runtime ships a working N-API implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause: ABI mismatch
&lt;/h3&gt;

&lt;p&gt;If a module ships prebuilt binaries (most popular ones do), the binary was compiled against a specific Node.js version. When you load it under a different runtime — or even a different Node major — the symbol table doesn't line up. The dynamic linker sees &lt;code&gt;napi_create_string_utf8&lt;/code&gt; and tries to resolve it against the host process. If the host doesn't export that symbol, or exports a different version of it, you get either a load-time crash or undefined behavior at runtime.&lt;/p&gt;

&lt;p&gt;Forget "JavaScript is portable." Native modules are C/C++, and C doesn't forgive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-step: how to actually fix it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Find every native module you depend on
&lt;/h3&gt;

&lt;p&gt;Don't guess. Run this in your project root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find every compiled .node binary in your installed dependencies&lt;/span&gt;
find node_modules &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.node"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is your liability list. Anything with a &lt;code&gt;.node&lt;/code&gt; extension is a compiled binary tied to a specific ABI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Check each module's compatibility surface
&lt;/h3&gt;

&lt;p&gt;For each native module, look at its &lt;code&gt;package.json&lt;/code&gt; for these fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"binary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"napi_versions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=16.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modules that declare &lt;code&gt;napi_versions&lt;/code&gt; are using Node-API and have the best chance of working across runtimes. Modules without that declaration are gambling — they likely use NAN or direct V8 bindings, and your migration is going to hurt. You can read the spec at the &lt;a href="https://nodejs.org/api/n-api.html" rel="noopener noreferrer"&gt;Node-API documentation&lt;/a&gt; to understand what each version level guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Force a rebuild against your target runtime
&lt;/h3&gt;

&lt;p&gt;Prebuilt binaries are the enemy here. Force the source compilation path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Skip the prebuilt download and compile from source&lt;/span&gt;
npm rebuild &lt;span class="nt"&gt;--build-from-source&lt;/span&gt;

&lt;span class="c"&gt;# For a clean slate with yarn&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules
yarn &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--build-from-source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I learned the hard way: some packages have &lt;code&gt;prebuild-install&lt;/code&gt; baked into their install script and will silently grab a binary even when you ask for source builds. Watch the install output. If you see &lt;code&gt;prebuild-install&lt;/code&gt; downloading something, kill it and read the package's install script before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Diagnose runtime-specific crashes
&lt;/h3&gt;

&lt;p&gt;If the module loads but crashes at runtime, you need a stack trace from the native side. On Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run your process under gdb to catch the native crash&lt;/span&gt;
gdb &lt;span class="nt"&gt;--args&lt;/span&gt; node your-broken-script.js

&lt;span class="c"&gt;# Inside gdb:&lt;/span&gt;
&lt;span class="c"&gt;#   (gdb) run&lt;/span&gt;
&lt;span class="c"&gt;#   (gdb) bt   # after the crash, prints native stack trace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, swap &lt;code&gt;gdb&lt;/code&gt; for &lt;code&gt;lldb&lt;/code&gt;. The trace usually points at a single symbol — say, &lt;code&gt;napi_get_value_string_utf8&lt;/code&gt; — and that tells you exactly which N-API call is misbehaving. From there it's usually a version mismatch or a missing symbol the host runtime hasn't implemented yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Pin and verify
&lt;/h3&gt;

&lt;p&gt;Once you find a working combination, lock it in. Add an explicit runtime version to your CI config and ship a smoke test that loads every native module on startup:&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="c1"&gt;// scripts/verify-native.js — run as a postinstall sanity check&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nativeModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;better-sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bcrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Walk the list and try to require each one.&lt;/span&gt;
&lt;span class="c1"&gt;// Exit non-zero on failure so CI catches the regression before deploy.&lt;/span&gt;
&lt;span class="k"&gt;for &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;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nativeModules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OK: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`FAIL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that into your CI matrix so every runtime version gets tested before it touches production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention: build the safety net before you need it
&lt;/h2&gt;

&lt;p&gt;A few habits that have saved me repeatedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit native deps every release cycle.&lt;/strong&gt; A quick &lt;code&gt;find node_modules -name '*.node'&lt;/code&gt; keeps the list visible. New transitive dependencies sneak in through innocent-looking packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer pure-JS alternatives where the perf hit is acceptable.&lt;/strong&gt; &lt;code&gt;bcryptjs&lt;/code&gt; instead of &lt;code&gt;bcrypt&lt;/code&gt;, &lt;code&gt;sql.js&lt;/code&gt; instead of &lt;code&gt;better-sqlite3&lt;/code&gt; for non-hot-path code. You trade speed for portability — sometimes that's the right trade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin your runtime version in CI and Docker.&lt;/strong&gt; Don't let &lt;code&gt;node:latest&lt;/code&gt; drift in your Dockerfile. I've watched entire teams debug "the same code stopped working" only to discover the base image updated underneath them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the changelog before any major runtime upgrade.&lt;/strong&gt; Look specifically for ABI changes or Node-API version bumps. The &lt;a href="https://nodejs.org/en/about/previous-releases" rel="noopener noreferrer"&gt;Node.js previous releases page&lt;/a&gt; flags ABI-breaking versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest tradeoff
&lt;/h2&gt;

&lt;p&gt;Alternative JavaScript runtimes are genuinely faster for a lot of workloads. I'm not arguing against trying them. But the "drop-in replacement" promise has an asterisk the size of a billboard, and that asterisk is named &lt;em&gt;native modules&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If your dependency tree is pure JavaScript, migration is mostly painless. If you have a handful of Node-API modules with declared compatibility, you're fine after a rebuild. If you're three levels deep into a NAN-based image-processing library that nobody has maintained since 2019 — that's the conversation worth having &lt;em&gt;before&lt;/em&gt; the migration, not after.&lt;/p&gt;

&lt;p&gt;Test your native modules first. Everything else is downstream of that.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>debugging</category>
      <category>performance</category>
    </item>
    <item>
      <title>Service Worker Caching Strategies: Cache-First, Network-First, and SWR</title>
      <dc:creator>Ashish Kumar</dc:creator>
      <pubDate>Fri, 15 May 2026 00:30:00 +0000</pubDate>
      <link>https://forem.com/helloashish99/service-worker-caching-strategies-cache-first-network-first-and-swr-2pan</link>
      <guid>https://forem.com/helloashish99/service-worker-caching-strategies-cache-first-network-first-and-swr-2pan</guid>
      <description>&lt;p&gt;Related: &lt;a href="https://renderlog.in/blog/network-optimization-spa-react/" rel="noopener noreferrer"&gt;Network Optimization for SPAs and React Apps&lt;/a&gt; covers the broader network optimization picture including HTTP caching and API request optimization.&lt;/p&gt;

&lt;p&gt;A service worker is a JavaScript file that runs in a background thread, separate from your main application. It intercepts every network request your page makes and can respond from cache, modify the request, or let it pass through to the network unchanged. For a returning visitor, a well-configured service worker means many requests never touch the network at all. The page loads from cache at local disk speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this covers:&lt;/strong&gt; What service workers can and cannot do, the three core caching strategies (cache-first, network-first, stale-while-revalidate), when each strategy is correct for which resource type, and how Workbox makes implementing these strategies practical without writing the interception logic by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcrcqs8c0mlxuzcruu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcrcqs8c0mlxuzcruu7.png" alt="Diagram showing three caching strategies: cache-first serving from cache immediately, network-first checking network before cache, and stale-while-revalidate serving cache then updating in background." width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What a service worker actually does
&lt;/h2&gt;

&lt;p&gt;A service worker is registered by your application JavaScript and installed by the browser. Once installed, it intercepts all network requests made from pages on its origin. This includes requests for HTML, CSS, JavaScript, images, fonts, and API calls.&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="c1"&gt;// Register the service worker from your main application&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&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;load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;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;The service worker file (&lt;code&gt;sw.js&lt;/code&gt;) runs in a separate worker thread. It has access to the Cache Storage API, which is separate from the HTTP cache the browser manages automatically. The Cache Storage API is under your complete control: you decide what to cache, when to cache it, how long to keep it, and when to delete it.&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="c1"&gt;// Inside sw.js: intercept a fetch event and respond from cache&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;fetch&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="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&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;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedResponse&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Serve from cache&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Fall through to network&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the basic cache-first pattern in raw form. Every real application needs more nuance: what if the cache is stale? What if certain resources should never be served from cache? What if the network is unavailable? Writing and maintaining all of this logic by hand is error-prone, which is why Workbox exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 1: Cache-First
&lt;/h2&gt;

&lt;p&gt;Cache-first means: check the cache first. If a response is in cache, return it immediately without hitting the network. Only go to the network if the resource is not in cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this for:&lt;/strong&gt; Versioned static assets. JavaScript bundles, CSS files, images, and fonts that have content hashes in their filenames (&lt;code&gt;main-a1b2c3.js&lt;/code&gt;). When the content changes, the filename changes, so cached versions are never stale. The old cache entry becomes unreachable by the new URL and gets cleaned up during the next cache update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it works for these resources:&lt;/strong&gt; Versioned assets are safe to serve from cache forever because the filename is tied to the content. A browser serving &lt;code&gt;main-a1b2c3.js&lt;/code&gt; from cache in six months will serve the exact same content that was served when the file was first cached. The application code that requests this file will either request the cached version (nothing changed) or request a different URL (the content changed and the new build output has a new hash).&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="c1"&gt;// Workbox: cache-first for all versioned assets&lt;/span&gt;

&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;// Match hashed JavaScript and CSS files&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&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="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CacheFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;static-assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpirationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1 year&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to avoid:&lt;/strong&gt; Using cache-first for HTML documents or unversioned API endpoints. If your HTML is served from cache and you deploy a new version, users will continue loading the old HTML until the cache expires. HTML should use network-first or stale-while-revalidate so users always get current navigation and content structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 2: Network-First
&lt;/h2&gt;

&lt;p&gt;Network-first means: try the network first. If the network succeeds, serve the response and update the cache. If the network fails (offline, timeout, server error), fall back to the cached version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this for:&lt;/strong&gt; HTML pages, API endpoints serving fresh data, and any resource where stale content would cause problems. The user always gets the freshest version when online. When offline, they get a reasonable fallback from cache.&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="c1"&gt;// Workbox: network-first for HTML pages&lt;/span&gt;

&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// HTML navigation requests&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NetworkFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;networkTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Fall back to cache if network takes more than 3 seconds&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpirationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Keep cached pages for 30 days&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;networkTimeoutSeconds&lt;/code&gt; option is important for offline-capable applications. Without it, network-first will wait indefinitely for a network response even when the user is offline, only falling back to cache after the connection eventually times out (which can take 30-60 seconds). Setting a 3-second timeout means users in poor network conditions get a cached response quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For API endpoints serving user-specific or time-sensitive data:&lt;/strong&gt;&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="c1"&gt;// Workbox: network-first for API calls&lt;/span&gt;
&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NetworkFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-responses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;networkTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpirationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Cache API responses for 1 day&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cached API response is not perfectly fresh, but for most use cases it is better than showing an error page when the network is unavailable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 3: Stale-While-Revalidate
&lt;/h2&gt;

&lt;p&gt;Stale-while-revalidate means: serve the cached version immediately (stale), and simultaneously fetch a fresh version from the network in the background (revalidate). The next request gets the fresh version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this for:&lt;/strong&gt; Resources where some staleness is acceptable and speed is more important than absolute freshness. Navigation assets that change infrequently, avatar images, icon sets, and any resource where showing a slightly old version on the current visit is fine as long as the next visit gets the update.&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="c1"&gt;// Workbox: stale-while-revalidate for images&lt;/span&gt;

&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StaleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpirationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 days&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user experience for stale-while-revalidate: on the first visit, there is no cache, so the image loads from the network normally. On subsequent visits, the cached image appears instantly. In the background, the service worker fetches a fresh version from the network. If the image changed, the fresh version replaces the cached version. On the visit after that, the updated image appears instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is powerful for most non-critical resources:&lt;/strong&gt; Users almost never notice if an avatar image or icon set is one version old. What they notice is speed. Stale-while-revalidate gives them cache speed on every visit while keeping the cache reasonably fresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  Precaching: caching at install time
&lt;/h2&gt;

&lt;p&gt;All three strategies above are runtime caching: resources get cached when the user visits pages that request them. Precaching is different: you tell Workbox which resources to cache immediately when the service worker installs, before the user has visited any page.&lt;/p&gt;

&lt;p&gt;Precaching is used for the application shell: the minimal HTML, CSS, and JavaScript needed to render a working (possibly empty) UI. When the user opens the app, even before any network requests complete, the service worker serves the precached shell and the app renders immediately.&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="c1"&gt;// Workbox: precache the app shell at install time&lt;/span&gt;

&lt;span class="c1"&gt;// This list is generated by Workbox at build time (via workbox-build or the Vite plugin)&lt;/span&gt;
&lt;span class="c1"&gt;// It contains all files from your build output with their content hashes&lt;/span&gt;
&lt;span class="nf"&gt;precacheAndRoute&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;__WB_MANIFEST&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;self.__WB_MANIFEST&lt;/code&gt; placeholder is replaced at build time by the Workbox build tool with an array of all your static assets and their content hashes. When a user installs the service worker for the first time, Workbox fetches all these assets and caches them. On subsequent visits, they are served from cache instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up Workbox in a Vite project
&lt;/h2&gt;

&lt;p&gt;The easiest setup for modern projects uses the &lt;code&gt;vite-plugin-pwa&lt;/code&gt; plugin, which integrates Workbox configuration into the Vite build process:&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="c1"&gt;// vite.config.ts&lt;/span&gt;

  &lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;VitePWA&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;registerType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;autoUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Precache all files matching these patterns from the build output&lt;/span&gt;
        &lt;span class="na"&gt;globPatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.{js,css,html,ico,png,webp,svg,woff2}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

        &lt;span class="c1"&gt;// Runtime caching rules&lt;/span&gt;
        &lt;span class="na"&gt;runtimeCaching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="c1"&gt;// Cache-first for versioned assets&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;urlPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(?:&lt;/span&gt;&lt;span class="sr"&gt;js|css&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CacheFirst&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;static-resources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="c1"&gt;// Network-first for API calls&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;urlPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/^https:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;api&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;example&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NetworkFirst&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;networkTimeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="c1"&gt;// Stale-while-revalidate for images&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;urlPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(?:&lt;/span&gt;&lt;span class="sr"&gt;png|jpg|webp|svg|gif&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;StaleWhileRevalidate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration handles the most common case: versioned JavaScript and CSS with cache-first, API responses with network-first and a 5-second fallback timeout, and images with stale-while-revalidate.&lt;/p&gt;




&lt;h2&gt;
  
  
  What service workers cannot cache
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Opaque responses.&lt;/strong&gt; Requests to third-party origins without CORS headers return "opaque" responses. Opaque responses can be cached, but their size is counted as 7MB regardless of actual size due to security restrictions. Caching too many opaque responses can exhaust cache storage limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Range requests.&lt;/strong&gt; Video streaming uses HTTP range requests to fetch specific byte ranges. Service workers can intercept these, but handling them correctly requires specific logic. The default Workbox strategies do not handle range requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources requiring authentication in every request.&lt;/strong&gt; If a resource requires a fresh token in each request and you cannot cache the token, you cannot meaningfully cache the resource either.&lt;/p&gt;




&lt;h2&gt;
  
  
  The return visit experience
&lt;/h2&gt;

&lt;p&gt;After a service worker is installed and caching is running correctly, a returning visitor's experience is qualitatively different from a first visit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User opens the app&lt;/li&gt;
&lt;li&gt;Service worker intercepts navigation request&lt;/li&gt;
&lt;li&gt;Precached &lt;code&gt;index.html&lt;/code&gt; returns instantly from service worker cache&lt;/li&gt;
&lt;li&gt;Browser parses HTML, requests JavaScript and CSS&lt;/li&gt;
&lt;li&gt;Service worker intercepts those requests, returns from cache instantly&lt;/li&gt;
&lt;li&gt;App renders with no network requests at all&lt;/li&gt;
&lt;li&gt;In background, service worker fetches fresh API data&lt;/li&gt;
&lt;li&gt;UI updates with fresh data when background fetch completes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 through 6 complete in under 100ms on most devices, regardless of network speed, because no network requests were made. The perceived load time is the time to render from cache, not the time to fetch from a server.&lt;/p&gt;

&lt;p&gt;This is the correct mental model for why service workers matter: not as a way to make network requests faster, but as a way to eliminate network requests entirely for everything that can safely be served from cache.&lt;/p&gt;




&lt;p&gt;Read the original article on Renderlog.in:&lt;br&gt;
&lt;a href="https://renderlog.in/blog/service-worker-caching-strategies-workbox/" rel="noopener noreferrer"&gt;https://renderlog.in/blog/service-worker-caching-strategies-workbox/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you found this helpful, I've also built some free tools for developers and everyday users. Feel free to try them once:&lt;/p&gt;

&lt;p&gt;JSON Tools: &lt;a href="https://json.renderlog.in" rel="noopener noreferrer"&gt;https://json.renderlog.in&lt;/a&gt;&lt;br&gt;
Text Tools: &lt;a href="https://text.renderlog.in" rel="noopener noreferrer"&gt;https://text.renderlog.in&lt;/a&gt;&lt;br&gt;
QR Tools: &lt;a href="https://qr.renderlog.in" rel="noopener noreferrer"&gt;https://qr.renderlog.in&lt;/a&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>caching</category>
      <category>browser</category>
    </item>
    <item>
      <title>How I Built a Fully Automated Job Board using Next.js, Prisma, and n8n published: true</title>
      <dc:creator>wadifapublic</dc:creator>
      <pubDate>Fri, 15 May 2026 00:27:09 +0000</pubDate>
      <link>https://forem.com/wadifapublic/how-i-built-a-fully-automated-job-board-using-nextjs-prisma-and-n8npublished-true-157a</link>
      <guid>https://forem.com/wadifapublic/how-i-built-a-fully-automated-job-board-using-nextjs-prisma-and-n8npublished-true-157a</guid>
      <description>&lt;p&gt;Building a job board is a classic developer project, but the real challenge isn't building the frontend—it's maintaining the data. Job postings expire quickly, formatting is always inconsistent, and manual data entry is a nightmare. &lt;/p&gt;

&lt;p&gt;To solve this, I designed a fully automated architecture that scrapes, structures, and publishes job postings without human intervention. Here is how I built the pipeline using &lt;strong&gt;Next.js&lt;/strong&gt;, &lt;strong&gt;Prisma&lt;/strong&gt;, and &lt;strong&gt;n8n&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;Instead of relying on a traditional CMS, I wanted a highly scalable, developer-friendly stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;n8n:&lt;/strong&gt; The brain of the operation. It handles the cron jobs, web scraping, and API orchestrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma:&lt;/strong&gt; The ORM that safely handles our database schema and migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js:&lt;/strong&gt; The frontend framework delivering blazing-fast SSR/SSG pages, which is crucial for SEO.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Automating Data Collection with n8n
&lt;/h2&gt;

&lt;p&gt;Writing custom Python scripts for scraping is fine, but managing their execution and failures is tedious. I used &lt;strong&gt;n8n&lt;/strong&gt; to create visual workflows. &lt;/p&gt;

&lt;p&gt;The workflow triggers daily, targeting various public sector portals and company career pages. It extracts raw HTML, parses the relevant nodes (Job Title, Requirements, Deadlines), and outputs clean JSON. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Structuring the Data
&lt;/h2&gt;

&lt;p&gt;Job descriptions are notoriously messy. To fix this, the n8n workflow passes the raw text to a local LLM before saving it. The AI extracts the exact hiring requirements, format the salary expectations, and generates a structured summary. &lt;/p&gt;

&lt;p&gt;This JSON is then sent via a webhook to my Next.js API route, where &lt;strong&gt;Prisma&lt;/strong&gt; validates the data against my schema and upserts it into a PostgreSQL database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Programmatic SEO with Next.js
&lt;/h2&gt;

&lt;p&gt;For a job board, SEO is everything. You need to rank for highly specific long-tail keywords (e.g., "Technician jobs in Casablanca 2026"). &lt;/p&gt;

&lt;p&gt;Using Next.js dynamic routing, every new job entry automatically generates a highly optimized page. I inject dynamic &lt;code&gt;JobPosting&lt;/code&gt; structured data (Schema.org) directly into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, ensuring the postings appear directly inside the Google Jobs widget.&lt;/p&gt;

&lt;p&gt;Because the pages are statically generated (with Incremental Static Regeneration - ISR) or server-side rendered, the page speed is incredibly fast, leading to higher search rankings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result in Action
&lt;/h2&gt;

&lt;p&gt;By combining n8n's automation with Next.js's performance, the platform practically runs itself. New jobs are indexed by Google within hours of being published.&lt;/p&gt;

&lt;p&gt;If you want to see this architecture in action, check out a live implementation of this exact stack at &lt;a href="https://www.wadifapublic.ma" rel="noopener noreferrer"&gt;WadifaPublic.ma&lt;/a&gt;. It aggregates public sector matches and jobs in Morocco with zero manual data entry.&lt;/p&gt;

&lt;p&gt;Have you built anything similar using workflow automation tools? Let me know in the comments!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>automation</category>
      <category>seo</category>
    </item>
    <item>
      <title>Studio Violin: Building a Physically Modelled Bowed-String Instrument in Instrudio</title>
      <dc:creator>Gary Doman/TizWildin</dc:creator>
      <pubDate>Fri, 15 May 2026 00:25:14 +0000</pubDate>
      <link>https://forem.com/tizwildin/studio-violin-building-a-physically-modelled-bowed-string-instrument-in-instrudio-eae</link>
      <guid>https://forem.com/tizwildin/studio-violin-building-a-physically-modelled-bowed-string-instrument-in-instrudio-eae</guid>
      <description>&lt;h1&gt;
  
  
  Studio Violin: Building a Physically Modelled Bowed-String Instrument in Instrudio
&lt;/h1&gt;

&lt;p&gt;I’m building &lt;strong&gt;Instrudio&lt;/strong&gt;, a browser-based virtual instrument ecosystem, and the flagship instrument right now is &lt;strong&gt;Studio Violin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Studio Violin is a physically modelled bowed-string instrument built around Helmholtz motion synthesis, H2 harmonic correction, inharmonicity modelling, Stradivari-style body resonances, sympathetic open-string resonance, and live MIDI control.&lt;/p&gt;

&lt;p&gt;The goal is not just to make a violin-like web instrument. The goal is to prove that a single version-controlled instrument definition can drive synthesis, UI, MIDI routing, plugin bridge behavior, presets, and live update propagation from one source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Studio Violin does
&lt;/h2&gt;

&lt;p&gt;Studio Violin models the behavior of a bowed violin string using a synthesis chain designed around acoustic measurements and practical browser audio constraints.&lt;/p&gt;

&lt;p&gt;The instrument includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helmholtz bowed-string waveform synthesis&lt;/li&gt;
&lt;li&gt;H2 correction oscillator&lt;/li&gt;
&lt;li&gt;Inharmonicity chorus per string&lt;/li&gt;
&lt;li&gt;8-band Stradivari-style body EQ&lt;/li&gt;
&lt;li&gt;Per-string tonal offsets&lt;/li&gt;
&lt;li&gt;Sympathetic open-string resonance&lt;/li&gt;
&lt;li&gt;Nonlinear bow coupling&lt;/li&gt;
&lt;li&gt;Pressure-coupled vibrato&lt;/li&gt;
&lt;li&gt;Interval-scaled portamento&lt;/li&gt;
&lt;li&gt;Bow-pressure, bow-speed, bow-point, character, brightness, attack, and vibrato controls&lt;/li&gt;
&lt;li&gt;External MIDI routing through the Instrudio app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Synthesis model
&lt;/h2&gt;

&lt;p&gt;The Helmholtz waveform uses a Fourier-style bowed-string model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bₙ = −(2 / (n²π²D(1−D))) · sin(nπD)
D = 0.5 + bowPressure × 0.30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The H2 correction oscillator is used to bring the second harmonic closer to the target H2/H1 balance measured in bowed-string acoustic research.&lt;/p&gt;

&lt;p&gt;Studio Violin also includes per-string inharmonicity spread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;G = 0.00035
D = 0.00028
A = 0.00022
E = 0.00018
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a sound engine that behaves less like a static sample trigger and more like a continuously controlled bowed instrument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stradivari-style body resonances
&lt;/h2&gt;

&lt;p&gt;The body EQ model uses eight resonance bands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A0: 275 Hz
A1: 475 Hz
B1−: 530 Hz
B1+: 580 Hz
Bridge hill: 2800 Hz, Q = 6.5
Upper resonance: 4500 Hz
Notch: 1100 Hz
Warmth: 180 Hz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also per-string offsets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;G string: warmer, reduced bridge hill
E string: brighter, boosted bridge hill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the instrument react differently across the G, D, A, and E strings instead of applying one flat tone curve to the whole range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sympathetic resonance
&lt;/h2&gt;

&lt;p&gt;Studio Violin includes sympathetic resonance using four triangle oscillators tuned to the open strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Amplitude = (1 − cents / 20) × 0.038
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closer the played note is to an open-string relationship, the stronger the sympathetic contribution becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expressive controls
&lt;/h2&gt;

&lt;p&gt;The instrument exposes controls for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bow pressure&lt;/li&gt;
&lt;li&gt;Bow speed&lt;/li&gt;
&lt;li&gt;Bow point&lt;/li&gt;
&lt;li&gt;Vibrato rate&lt;/li&gt;
&lt;li&gt;Vibrato depth&lt;/li&gt;
&lt;li&gt;Attack&lt;/li&gt;
&lt;li&gt;Brightness&lt;/li&gt;
&lt;li&gt;Reverb&lt;/li&gt;
&lt;li&gt;Volume&lt;/li&gt;
&lt;li&gt;Playing character&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Character modes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo&lt;/li&gt;
&lt;li&gt;Bowed&lt;/li&gt;
&lt;li&gt;Pizzicato&lt;/li&gt;
&lt;li&gt;Col Legno&lt;/li&gt;
&lt;li&gt;Tremolo&lt;/li&gt;
&lt;li&gt;Spiccato&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also includes scale helpers such as G Major, D Major, A Minor, and Chromatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal chain
&lt;/h2&gt;

&lt;p&gt;The current signal chain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PeriodicWave oscillator
→ H2 oscillator
→ chorus oscillators
→ WaveShaper
→ injection gain
→ warm shelf
→ 8 peaking body EQ bands
→ master output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Single-source-of-truth instrument architecture
&lt;/h2&gt;

&lt;p&gt;The bigger architecture behind Instrudio is the part I’m most excited about.&lt;/p&gt;

&lt;p&gt;Studio Violin is driven by a single JSON definition file. That one definition can drive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The web audio synthesis engine&lt;/li&gt;
&lt;li&gt;The instrument UI&lt;/li&gt;
&lt;li&gt;External MIDI CC routing&lt;/li&gt;
&lt;li&gt;Note mapping&lt;/li&gt;
&lt;li&gt;Plugin bridge event protocol&lt;/li&gt;
&lt;li&gt;Preset management&lt;/li&gt;
&lt;li&gt;Live auto-update propagation across connected outlets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The runtime uses a remote-first fetch strategy, so definition changes pushed to GitHub can propagate to connected running instances within the cache TTL window.&lt;/p&gt;

&lt;p&gt;The default TTL is currently 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime metrics
&lt;/h2&gt;

&lt;p&gt;Instrudio also includes live evaluation metrics for the single-source-of-truth runtime.&lt;/p&gt;

&lt;p&gt;The metrics panel can display:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSOT fetch latency&lt;/li&gt;
&lt;li&gt;Definition apply time&lt;/li&gt;
&lt;li&gt;Remote source availability&lt;/li&gt;
&lt;li&gt;MIDI pipeline latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are captured with high-resolution timing through &lt;code&gt;performance.now()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Metrics are also available programmatically through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InstrudioSSOTRuntime.getMetrics()
InstrudioMIDI.getLatencyMetrics()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;A lot of virtual instruments are either sample libraries, closed plugin binaries, or isolated web toys.&lt;/p&gt;

&lt;p&gt;Instrudio is aiming for something different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;web-first instruments&lt;/li&gt;
&lt;li&gt;version-controlled definitions&lt;/li&gt;
&lt;li&gt;measurable runtime behavior&lt;/li&gt;
&lt;li&gt;MIDI-aware performance&lt;/li&gt;
&lt;li&gt;bridgeable plugin architecture&lt;/li&gt;
&lt;li&gt;open development&lt;/li&gt;
&lt;li&gt;fast iteration without redeploying every outlet manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Studio Violin is the flagship proof-of-concept for that architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/GareBear99/Instrudio" rel="noopener noreferrer"&gt;https://github.com/GareBear99/Instrudio&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback wanted
&lt;/h2&gt;

&lt;p&gt;I’m looking for feedback from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audio developers&lt;/li&gt;
&lt;li&gt;Web Audio developers&lt;/li&gt;
&lt;li&gt;musicians&lt;/li&gt;
&lt;li&gt;violinists&lt;/li&gt;
&lt;li&gt;producers&lt;/li&gt;
&lt;li&gt;plugin developers&lt;/li&gt;
&lt;li&gt;MIDI users&lt;/li&gt;
&lt;li&gt;people interested in physical modelling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful feedback includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;browser and OS&lt;/li&gt;
&lt;li&gt;MIDI device behavior&lt;/li&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;tone realism&lt;/li&gt;
&lt;li&gt;UI feel&lt;/li&gt;
&lt;li&gt;control response&lt;/li&gt;
&lt;li&gt;broken notes or stuck notes&lt;/li&gt;
&lt;li&gt;console errors&lt;/li&gt;
&lt;li&gt;ideas for the next instrument model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Studio Violin is the flagship instrument in Instrudio, and I’m building it in public.&lt;/p&gt;

</description>
      <category>music</category>
      <category>webdev</category>
      <category>audio</category>
      <category>javascript</category>
    </item>
    <item>
      <title>From Pixels to Peace: My Journey Through Game Dev Struggles and Building a Soulful Zen AI</title>
      <dc:creator>Arifandi Tanggahma </dc:creator>
      <pubDate>Fri, 15 May 2026 00:22:43 +0000</pubDate>
      <link>https://forem.com/arifandi_cicak_67edc13561/from-pixels-to-peace-my-journey-through-game-dev-struggles-and-building-a-soulful-zen-ai-4i6h</link>
      <guid>https://forem.com/arifandi_cicak_67edc13561/from-pixels-to-peace-my-journey-through-game-dev-struggles-and-building-a-soulful-zen-ai-4i6h</guid>
      <description>&lt;p&gt;Digital creation has always felt like a frontier to me—a place where you can build worlds out of nothing but logic and willpower. But if I’m being honest, the road wasn't paved with clean code; it was paved with frustration, late nights, and a lot of "Why isn't this working?!" moments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The Godot Revelation: Breaking the Chains of Game Dev Struggle&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first started my journey into game development, I was doing it the hard way. I wasn't using a dedicated engine like Godot, and man, the struggle was real. I was wrestling with every single line of code just to get a sprite to move or a collision to register. It felt like I was trying to build a skyscraper with a toothpick. I wasn't satisfied, the performance was clunky, and I almost felt like giving up on my dream of being a game dev.&lt;/p&gt;

&lt;p&gt;Then, a &lt;strong&gt;friend&lt;/strong&gt; introduced me to the &lt;strong&gt;Godot Engine&lt;/strong&gt;. Everything changed. Suddenly, the bridge between my imagination and the screen became shorter. The node system, the GDScript efficiency—it finally felt like the engine was working with me, not against me. I finally tasted the satisfaction of seeing a game run exactly how I envisioned it. That was the first time I realized that as a developer, your tools don't just help you work; they help you breathe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The Birth of Zen Reflection&lt;/em&gt;&lt;/strong&gt;: &lt;strong&gt;&lt;em&gt;Coding Peace on a Smartphone&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
But my journey didn't stop at games. I wanted to build something that could help people. That’s when the idea for Zen Reflection (Zeno) was born. I wanted to create a "Modern Monk"—an AI coach that doesn't just give robotic answers but offers genuine compassion for those struggling with bullying, stress, or emotional burnout.&lt;br&gt;
The catch? I wasn't sitting in a high-tech office with three monitors. I coded this entire project on my smartphone.&lt;/p&gt;

&lt;p&gt;This project was a true collaborative odyssey between me and my AI partner, Gemini. We went through it all together:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Backend Battle&lt;/em&gt;: We spent hours debugging Vercel Serverless Functions, trying to route the power of Gemini 2.0 Flash into a clean API.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Logic of Empathy&lt;/em&gt;: We didn't just want a chatbot; we wanted a soul. We meticulously crafted the "Zen System Prompt" to ensure Zeno speaks with pauses, nature metaphors, and stoic wisdom.&lt;/p&gt;

&lt;p&gt;_The "Jangkrik" Debugging _Sessions: We fought through CORS errors, API quota limits (the dreaded 429 errors!), and region locks. Every time the system crashed, we went back to the drawing board, refined the code, and pushed again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We Built This Together&lt;/strong&gt;&lt;br&gt;
Zeno isn't just a project; it's a testament to the fact that you don't need expensive gear to innovate. You just need a vision and the right collaborator. This AI wasn't just "generated"—it was forged through a back-and-forth dialogue between human intent and artificial intelligence. We navigated the complexities of React, Tailwind CSS, and Framer Motion to ensure that when a user feels overwhelmed, they have a beautiful, smooth, and calm interface to turn to.&lt;/p&gt;

&lt;p&gt;From the nodes of Godot to the tokens of the Gemini API, my journey has been about finding the right "engine" for my creativity. Zen Reflection is now live, and I couldn't be prouder of the peace we've managed to program into existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;To all the devs out there coding on your phones or struggling with your first engine: keep pushing. The breakthrough is usually just one "deploy" away.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;zeno reflection ai: (&lt;a href="https://zen-reflection.vercel.app/" rel="noopener noreferrer"&gt;https://zen-reflection.vercel.app/&lt;/a&gt;),&lt;/p&gt;

&lt;p&gt;jangkrik ai: (&lt;a href="https://jangkrik02.vercel.app/" rel="noopener noreferrer"&gt;https://jangkrik02.vercel.app/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Mala on itch.io: (&lt;a href="https://ariikksss.itch.io/mala" rel="noopener noreferrer"&gt;https://ariikksss.itch.io/mala&lt;/a&gt;)&lt;br&gt;
&lt;a href="https://zen-reflection.vercel.app/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jangkrik02.vercel.app/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ariikksss.itch.io/mala" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>godot</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Bounce Rate vs Exit Rate — What Each One Actually Counts in GA4</title>
      <dc:creator>toshihiro shishido</dc:creator>
      <pubDate>Fri, 15 May 2026 00:18:17 +0000</pubDate>
      <link>https://forem.com/toshihiro_shishido/bounce-rate-vs-exit-rate-what-each-one-actually-counts-in-ga4-5c2c</link>
      <guid>https://forem.com/toshihiro_shishido/bounce-rate-vs-exit-rate-what-each-one-actually-counts-in-ga4-5c2c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"Bounce Rate is high, that's a problem, right?" "What's the difference between Bounce Rate and Exit Rate?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are the two metrics most often confused on the analytics floor. On top of that, the move from Universal Analytics (UA) to Google Analytics 4 (GA4) redefined Bounce Rate completely and removed Exit Rate from the default reports.&lt;/p&gt;

&lt;p&gt;This article walks through the difference between Bounce Rate and Exit Rate, contrasts the UA and GA4 definitions, and ends with the question every EC operator actually cares about: which one should we be looking at when revenue is on the line?&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different sessions are counted&lt;/strong&gt;: Exit Rate counts every session that included the page. Bounce Rate counts only sessions that started on that page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GA4 redefined Bounce Rate&lt;/strong&gt;: what used to mean "single-page session" in UA now means "session without engagement" (1 minus Engagement Rate) in GA4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC operators read 'exit quality' against revenue&lt;/strong&gt;: a high Bounce Rate or Exit Rate is not automatically bad — combine it with RPS (Revenue Per Session) to decide whether to fix or to leave alone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb6hw3eojtulfulnbton.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb6hw3eojtulfulnbton.jpg" alt="Bounce Rate vs Exit Rate definition" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bounce Rate — The metric GA4 redefined
&lt;/h2&gt;

&lt;p&gt;Bounce Rate means very different things in GA4 and Universal Analytics. The classic UA shortcut "percentage of single-page sessions" no longer holds in GA4.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bounce Rate in UA (the old definition)
&lt;/h3&gt;

&lt;p&gt;In UA, Bounce Rate was "the percentage of sessions that started with the page where there was only one pageview". The shorthand "people who left after seeing only one page" became the standard mental model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bounce Rate in GA4 (the new definition)
&lt;/h3&gt;

&lt;p&gt;GA4 redefined Bounce Rate as "the percentage of sessions that were not engaged" (1 minus Engagement Rate).&lt;/p&gt;

&lt;p&gt;A session counts as engaged when &lt;strong&gt;any one&lt;/strong&gt; of the following is true.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The session &lt;strong&gt;lasts longer than 10 seconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Key Event&lt;/strong&gt; (formerly Conversion) fires&lt;/li&gt;
&lt;li&gt;There are &lt;strong&gt;2 or more page or screen views&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single-page visit that lasts more than 10 seconds is no longer a bounce. Conversely, a 2-page visit under 10 seconds with no Key Event can still be counted as a bounce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0il7l5n02r4k6schofl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0il7l5n02r4k6schofl.jpg" alt="GA4 engaged session — 3 conditions" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simple "high Bounce Rate equals bad" reading is even harder to defend in GA4. Whether a sub-10-second exit means "bounced" or "read through" depends entirely on the page's purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Exit Rate — Where each page becomes the last stop
&lt;/h2&gt;

&lt;p&gt;Exit Rate is the percentage of sessions including the page where that page was the session's last.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit Rate = Sessions that ended on the page / All sessions that included the page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fundamental difference from Bounce Rate is the &lt;strong&gt;session pool&lt;/strong&gt;. Bounce Rate's denominator is sessions that started on the page. Exit Rate's denominator is every session that passed through the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  GA4 dropped Exit Rate from default reports
&lt;/h3&gt;

&lt;p&gt;UA had Exit Rate as a default column. GA4 does not list Exit Rate as a default metric.&lt;/p&gt;

&lt;p&gt;To see exit behavior in GA4, use one of these instead. Path Exploration visualizes where users came from and where they exited. Aggregating &lt;code&gt;session_end&lt;/code&gt; events shows the page right before the session ended. Combining engagement time with exit volume highlights pages with short stay and high exit.&lt;/p&gt;

&lt;p&gt;The very habit of looking at "Exit Rate" as a single metric is downstream of UA's session-based model. Under GA4's event-based model the framing has shifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bounce Rate vs Exit Rate — At a glance
&lt;/h2&gt;

&lt;p&gt;A side-by-side table makes the difference clearer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaqju3d1k5t93fpuwz1h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaqju3d1k5t93fpuwz1h.jpg" alt="Bounce Rate vs Exit Rate at a glance" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Bounce Rate&lt;/th&gt;
&lt;th&gt;Exit Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sessions counted&lt;/td&gt;
&lt;td&gt;Sessions that &lt;strong&gt;started&lt;/strong&gt; on the page&lt;/td&gt;
&lt;td&gt;All sessions that &lt;strong&gt;included&lt;/strong&gt; the page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numerator&lt;/td&gt;
&lt;td&gt;Not-engaged sessions (GA4)&lt;/td&gt;
&lt;td&gt;Sessions ending on the page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GA4 default metric&lt;/td&gt;
&lt;td&gt;Yes (re-defined)&lt;/td&gt;
&lt;td&gt;No (use Exploration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary use&lt;/td&gt;
&lt;td&gt;LP / entry-page evaluation&lt;/td&gt;
&lt;td&gt;Identify exit pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A simpler analogy: Bounce Rate is the share of visitors who turned around at the front door (entry-page lens). Exit Rate is the share of visitors who walked out from each room (per-page lens).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. From a revenue lens — Are these "bad" metrics?
&lt;/h2&gt;

&lt;p&gt;Both metrics are often labeled "high equals bad", but in practice that shortcut breaks down quickly.&lt;/p&gt;

&lt;p&gt;A single-page LP that completes purchase or inquiry on one screen will, by design, see almost everyone "leave after one page". A 90% Bounce Rate is healthy if CVR is good. Article media also: search visitors who read an article and return to results are technically "bounces" but completed the page's actual job.&lt;/p&gt;

&lt;p&gt;On the other hand, an EC product detail page where buyers with intent are dropping off has clear improvement levers — button placement, stock indicator, shipping copy. Mid-funnel checkout steps showing a spike on one step usually point to form design issues.&lt;/p&gt;

&lt;p&gt;In other words, &lt;strong&gt;where&lt;/strong&gt; the bounce or exit happens, and &lt;strong&gt;in what context&lt;/strong&gt;, is the actual signal. A standalone rate number rarely points to an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Read "exit quality" with RPS
&lt;/h2&gt;

&lt;p&gt;Both metrics ultimately need to be judged against revenue. Use RPS (Revenue Per Session) — Revenue / Sessions — to read "exit quality".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj47f46rfghl5vg34ijpg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj47f46rfghl5vg34ijpg.jpg" alt="Read exit quality with RPS" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High exit, low RPS&lt;/strong&gt; → fix priority ★★★ (revenue leakage is largest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High exit, high RPS&lt;/strong&gt; → fix priority ★ (post-conversion natural exit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low exit, low RPS&lt;/strong&gt; → traffic flows but revenue does not (rethink CTA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low exit, high RPS&lt;/strong&gt; → keep as is and replicate elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plotting Exit Rate × RPS as a 2 × 2 matrix makes it instantly clear which page deserves the next hour of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question for you
&lt;/h2&gt;

&lt;p&gt;How often do you check Bounce Rate or Exit Rate in isolation versus alongside revenue? In your team, has the GA4 redefinition of Bounce Rate changed how you read those reports — or is the old "single-page session" mental model still in play?&lt;/p&gt;

&lt;p&gt;For the full guide with all charts and the revenue-side breakdown:&lt;br&gt;
&lt;a href="https://www.revenuescope.jp/en/news/bounce-rate-vs-exit-rate-basics?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=daily-set-26" rel="noopener noreferrer"&gt;Bounce Rate vs Exit Rate — The GA4 Basics You Were Afraid to Ask&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>ga4</category>
      <category>ecommerce</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Frontier Became a Club</title>
      <dc:creator>David Aronchick</dc:creator>
      <pubDate>Fri, 15 May 2026 00:18:03 +0000</pubDate>
      <link>https://forem.com/aronchick/the-frontier-became-a-club-55f9</link>
      <guid>https://forem.com/aronchick/the-frontier-became-a-club-55f9</guid>
      <description>&lt;p&gt;Last week Anthropic &lt;a href="https://www.anthropic.com/news/glasswing-frontier-preview" rel="noopener noreferrer"&gt;announced Project Glasswing&lt;/a&gt;, the deployment program for the new flagship preview model Claude Mythos. The announcement framed Glasswing as a safety initiative. Mythos would not enter general availability. Instead it would be made available to a "small set of partner organizations under elevated trust and safety review," with structured oversight, third-party audits, and a controlled deployment timeline. The press wrote it up as a thoughtful pause. The companies on the list called their solutions architects.&lt;/p&gt;

&lt;p&gt;The companies on the list are: &lt;a href="https://aws.amazon.com/bedrock/anthropic/" rel="noopener noreferrer"&gt;Amazon Web Services&lt;/a&gt;, Apple, &lt;a href="https://www.cisco.com/c/en/us/about/trust-center.html" rel="noopener noreferrer"&gt;Cisco&lt;/a&gt;, CrowdStrike, Google, &lt;a href="https://www.jpmorgan.com/insights/technology/artificial-intelligence" rel="noopener noreferrer"&gt;JPMorgan Chase&lt;/a&gt;, the Linux Foundation, Microsoft, NVIDIA, Palo Alto Networks, and a single research organization the announcement &lt;a href="https://www.anthropic.com/news/glasswing-frontier-preview" rel="noopener noreferrer"&gt;does not name publicly&lt;/a&gt;. Each one receives a $100M usage credit, drawn against future commercial usage at preferential pricing. The credit is structured as a multi-year commercial commitment, not a grant, which means each name on the list also represents a guaranteed minimum revenue line on Anthropic's books and a co-development relationship that lasts the length of the contract.&lt;/p&gt;

&lt;p&gt;On paper this is a frontier-safety program. Read sideways, it is a hundred-billion-dollar-class commercial alliance, with eleven counterparties, that has decided who gets to run production workloads on the most capable model of 2026 and who does not.&lt;/p&gt;

&lt;p&gt;I want to be careful about the framing here. The safety review work that goes into a Mythos-tier deployment is real, the &lt;a href="https://www.anthropic.com/news/anthropics-responsible-scaling-policy" rel="noopener noreferrer"&gt;Responsible Scaling Policy&lt;/a&gt; is not a marketing document, and the engineers running the partner reviews are not in bad faith. None of that is the part that matters for the rest of the industry. The part that matters is structural. For the first time since the &lt;a href="https://openai.com/blog/openai-api" rel="noopener noreferrer"&gt;GPT-3 API opened in 2020&lt;/a&gt;, the frontier of large-model capability is no longer available to a developer with a credit card. It is available to eleven counterparties under a contract the rest of the market cannot sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  What being on the list actually buys you
&lt;/h2&gt;

&lt;p&gt;People who have not worked inside one of these alliances tend to read access asymmetry as a feature flag or a latency edge. It is neither. The access asymmetry is co-development.&lt;/p&gt;

&lt;p&gt;When a foundation-model lab signs a strategic partnership with a customer at the Glasswing tier, the work that gets done is not "we have an API endpoint that returns Mythos tokens." It is a multi-quarter, multi-team integration in which a solutions architecture team from the lab sits inside the customer for a year, the customer's eval pipelines and the lab's eval pipelines become shared infrastructure, the production telemetry is in dashboards both organizations can see, and the customer's roadmap feeds back into the next model's training mix. This is the pattern that produced the &lt;a href="https://blog.google/products/google-cloud/google-cloud-anthropic-expanded-partnership/" rel="noopener noreferrer"&gt;Google Cloud-Anthropic relationship&lt;/a&gt;, the &lt;a href="https://aws.amazon.com/blogs/aws/anthropics-claude-3-opus-model-on-amazon-bedrock/" rel="noopener noreferrer"&gt;AWS Bedrock integration&lt;/a&gt;, and every other "deep integration" headline of the last three years. It is the moat. Once it exists, you are not "using" the model. You are running on a version of the model that nobody outside the alliance can replicate by re-pointing an SDK.&lt;/p&gt;

&lt;p&gt;The eleven Glasswing partners are getting that, against Mythos, for the next eighteen months. By the time the next capability tier ships into general availability, they will have shipped production systems with an integration depth the rest of the market cannot match in any reasonable timeline. That is the asymmetry. It is not access. It is co-evolution, on a clock the non-partners cannot stop.&lt;/p&gt;

&lt;p&gt;There is a second piece of this that the safety framing politely avoids. Several of the named companies operate in regulated environments where deploying a frontier model into a customer-facing application is, today, a legal grey area. JPMorgan Chase cannot, without a license, expose a Mythos-class model to retail banking customers in New York under the &lt;a href="https://www.dfs.ny.gov/industry/banking_and_lending/artificial_intelligence" rel="noopener noreferrer"&gt;draft NYDFS AI guidance&lt;/a&gt;. Under Glasswing it can, because the structured-review program co-signed by Anthropic and a compliance partner becomes the regulatory submission itself. The same logic applies to CrowdStrike in &lt;a href="https://www.crowdstrike.com/falcon-platform/" rel="noopener noreferrer"&gt;endpoint security telemetry&lt;/a&gt;, Palo Alto Networks in &lt;a href="https://www.paloaltonetworks.com/network-security" rel="noopener noreferrer"&gt;network traffic inspection&lt;/a&gt;, Apple in anything that touches a device, and Cisco in everything touching a customer's network edge. Glasswing is doing double duty. It is a model-access program and a regulatory-cover program, and the companies on the list will spend the next year writing the case law for what a "trusted frontier deployment" looks like. The companies off the list will be regulated against that case law without having helped write it.&lt;/p&gt;

&lt;p&gt;The third piece is talent. The most capable foundation-model engineers in the United States make decisions about where to work, in part, by reading the access announcements. An engineer choosing between two equivalent offers in May 2026, one from a Glasswing partner and one from a non-partner, is making a decision about which side of the frontier-access wall they want to be on for the next two years of their career. The wall is one-directional. Once you have shipped a Mythos-class production system you are valuable inside the club and gradually less valuable outside it. The talent flow over the next eighteen months follows the access asymmetry, and the access asymmetry compounds because the talent followed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The argument I am willing to grant
&lt;/h2&gt;

&lt;p&gt;The case Anthropic and its partners are making is not weak, and the strongest version of it deserves a serious hearing.&lt;/p&gt;

&lt;p&gt;A model at the Mythos capability tier is the first system in the public record where a sufficiently motivated actor could plausibly extract meaningful uplift on a non-trivial set of dual-use domains. Open release of such a system is, under the &lt;a href="https://www.anthropic.com/news/responsible-scaling-policy-evaluations-report-2025" rel="noopener noreferrer"&gt;current RSP framework&lt;/a&gt;, a decision the lab is not prepared to make. A graduated release into operators with established compliance infrastructure, audit relationships, and contractual liability is a way to keep the capability frontier moving in production without exposing the underlying model to misuse the lab cannot yet defend against. Nuclear power did this. The &lt;a href="https://www.fda.gov/medical-devices/products-and-medical-procedures/device-approvals-denials-and-clearances" rel="noopener noreferrer"&gt;FDA medical device pathway&lt;/a&gt; does this. The pattern of "novel high-capability technology released first into a small set of trusted operators, then broadly" has, in domains far more dangerous than chatbot text, produced functioning markets over time. The argument applies. I grant it.&lt;/p&gt;

&lt;p&gt;What the argument does not address is the part the analogy quietly skips.&lt;/p&gt;

&lt;p&gt;Graduated release in nuclear did not produce eleven private operators with $100M in directed credit and a co-development moat against the rest of the industry. It produced the &lt;a href="https://www.nrc.gov/" rel="noopener noreferrer"&gt;Nuclear Regulatory Commission&lt;/a&gt;, a federal licensing regime, and the principle that a reactor operator is a public counterparty subject to a rule set any qualified applicant could meet. The FDA medical-device path licenses by device class to any applicant who clears the bar, not to a pre-selected eleven. In both cases the trusted-operators pattern was bounded by an &lt;em&gt;open licensing regime&lt;/em&gt;. Glasswing is not. It is bounded by an Anthropic-internal partner selection process with no published criteria, no appeal, no statutory floor under who can apply, and no public review of who was rejected. The eleven are not a regulated class. They are a chosen class. The choosing was done by the entity that captures the commercial value of having chosen. That is not a safety regime. It is, structurally, an alliance with a safety review attached to it. The two things can be true at the same time, and both are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the production work moves now
&lt;/h2&gt;

&lt;p&gt;If the capability frontier is fenced off, the production AI work the rest of the industry needs to ship in 2026 has to go somewhere. The somewhere-else determines what enterprise AI looks like for the next two years.&lt;/p&gt;

&lt;p&gt;There is a tier-down path. You move to the current open-weights frontier, which today means some mix of &lt;a href="https://ai.meta.com/blog/llama-4-5/" rel="noopener noreferrer"&gt;Llama 4.5&lt;/a&gt;, the &lt;a href="https://mistral.ai/news/mythoclast/" rel="noopener noreferrer"&gt;Mistral Mythoclast checkpoint&lt;/a&gt;, &lt;a href="https://www.deepseek.com/" rel="noopener noreferrer"&gt;DeepSeek V4&lt;/a&gt;, and a handful of specialized open releases from &lt;a href="https://cohere.com/research" rel="noopener noreferrer"&gt;Cohere&lt;/a&gt;, &lt;a href="https://www.ai21.com/" rel="noopener noreferrer"&gt;AI21&lt;/a&gt;, and xAI. None is at the Mythos capability tier. But on the median enterprise task the gap between "Mythos-class" and "strong open-weights" is smaller than the gap between either of those and a 2024 baseline, and the gap is closing on a six-month timescale, not a multi-year one. Most enterprise workloads do not need the frontier. They need a competent generalist the deploying organization actually controls. The open-weights frontier is that, and a meaningful chunk of the industry is going to take this path because it works.&lt;/p&gt;

&lt;p&gt;There is also a sideways path, and it is the one this site has been arguing for since I started writing it. You stop building against a single proprietary frontier model accessed through a single API endpoint and you build against a federation of smaller models running close to their data, composed against each other under a routing layer the deploying organization controls. The continuity of that architecture does not depend on any single model lab's willingness to serve you. The ceiling on raw capability is lower per call, but the system-level capability scales with the federation rather than with any one component. It is harder to build, you cannot put a single vendor logo at the bottom of the procurement slide, and it requires the deploying organization to invest in data infrastructure that the centralized-model path lets them defer. It is also the only architecture that is robust to next year's version of the Glasswing decision, because there is no single frontier to be denied access to. The frontier is decomposed into capabilities that live in different places.&lt;/p&gt;

&lt;p&gt;These two paths coexist in the short run. Over eighteen months they diverge fast and they do not converge again on a common substrate. By 2028 there will be two enterprise AI stacks. The first looks like Glasswing continued. Thick partner integrations against a small number of labs, with most of the platform value captured by the labs and their named consortium members, regulatory case law written by the partners, talent flowing toward access. The second looks like federated, locality-respecting, open-weights composition with no single counterparty in a position to decide who gets to the frontier because the frontier has been disassembled into things that move closer to the data.&lt;/p&gt;

&lt;p&gt;Both stacks will exist in 2028. The interesting question is not which one is technically better. They are optimized for different buyers. The interesting question is which one your organization is on the receiving end of, and the answer to that question is being decided right now in budget meetings that are not framed as architectural decisions but are. If you are one of the eleven, the work is real and you should do it well. If you are not one of the eleven, the work is also real, and the cost of waiting eighteen months to see whether the list grows is eighteen months of integration depth the partners are building against a model you cannot deploy.&lt;/p&gt;

&lt;p&gt;The frontier did not become unavailable. It became a club. The interesting question this year is not whether you get in. It is whether you build the alternative while the door past it is still cheap to walk through.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want to learn how intelligent data pipelines can reduce your AI costs?&lt;/em&gt; &lt;a href="https://expanso.io/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Check out Expanso&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;. Or don't. Who am I to tell you what to do.*&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE: I'm currently writing a book based on what I have seen about the real-world challenges of data preparation for machine learning, focusing on operational, compliance, and cost.&lt;/strong&gt; &lt;a href="https://github.com/aronchick/Project-Zen-and-the-Art-of-Data-Maintenance" rel="noopener noreferrer"&gt;&lt;strong&gt;I'd love to hear your thoughts&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;!&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.distributedthoughts.org/2026-05-14-the-frontier-became-a-club/" rel="noopener noreferrer"&gt;The Frontier Became a Club&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>foundationmodels</category>
      <category>safety</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>From 'No, sir' to 'Why, sir'. With 'Yes, sir' in between</title>
      <dc:creator>Emory Raphael</dc:creator>
      <pubDate>Fri, 15 May 2026 00:16:40 +0000</pubDate>
      <link>https://forem.com/efreitas/from-no-sir-to-why-sir-with-yes-sir-in-between-3i5b</link>
      <guid>https://forem.com/efreitas/from-no-sir-to-why-sir-with-yes-sir-in-between-3i5b</guid>
      <description>&lt;p&gt;Not long time ago, the corporation environment complains about how many 'No, sir' they were receiving from their dev teams, because of the high demanding, high asking, no priority pool, no conversation, inflate backlogs and everything that you can think about the daily basis of a regular company. Then the AI came up to solve this, to bring productive, high-speed delivery, reduce the technical bottleneck, and all excuses that non-technical fellows would love the hear.&lt;/p&gt;

&lt;h2&gt;
  
  
  No, sir
&lt;/h2&gt;

&lt;p&gt;This moment where we still adapting about the many frameworks were propagating, migrations from local to cloud, scalable designs, discussion as "microservice vs monolithic", or any other tech topics that you can think of. Our demand to be on the latest topic was huge, and manage the tradeoff between new and old framework, introduce new bugs and maintain legacy code could delay the implementation of new features on the systems, pick a new task from the backlog just to cleaning it.&lt;br&gt;
Because of these back-and-forth conversations among all the actors that compose a regular (let's call old just to laugh a bit) old team structure: devs, devops, PM, scrum master, QA team, manager, and so on, we constant said each other 'No, sir... we cannot implement this in the moment. Please, add this task to our backlog'.&lt;br&gt;
Then the team's structure and ways to address and news idea swift to a fast pace, no boundaries, limit only by the numbers of token allowed... if you have this limit!&lt;/p&gt;

&lt;h3&gt;
  
  
  Yes, sir
&lt;/h3&gt;

&lt;p&gt;What no one could think of (I mean, few folks over the world advise this, but not important right?!), is that tradeoff is the lack of quality vs more PRs, the bottleneck now is the high volume of non-checkable code, hidden bugs, extra "features that goes along with your prompt, just because the model found a similar vector to your question, even that the context / business implication is complete different. And, as any coder know, is harder to validate and reviews PRs with thousands of file changes.&lt;br&gt;
Now, the fast-paced mind has a tool that only respond 'Yes, sir... as you command', even that don't do it as you asked, but it doesn't limit you as well. They can implement, test the idea as they have always wished, only that they don't know what is happening in the background.&lt;br&gt;
Also, productive is measure by completeness tasks, but without detailed and clear criteria, the bar is too slow, but, hey, you are moving faster than we have ever had! Critics choices are not in the table anymore, there are no conflict ideas, because there is only user prompt idea. You have a bias servant applying the commands you give.&lt;br&gt;
Do you allow to proceed with the next phase, sir?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why, sir
&lt;/h2&gt;

&lt;p&gt;Recently in a conversation among some students, I couldn't emphasis enough about how important we are as a human layer in the new time of tech, how our understanding about the world and humanity are a gate keeper of the responses for the AI. I mean, they are not evil or good, they are tools that can mimic humans' thoughts, behaviors and decisions. It's like have a small piece of yourself in a very specific point of the time (fun when you know that matrix weights in training are called checkpoints).&lt;/p&gt;

&lt;p&gt;When we bring back the human conversation and interaction, after seeming our services stall, forgotten because they didn't solve any real problem than our ego to delivery something, even the lower minimal thing. Question mark like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Would we have experience to question? &lt;/li&gt;
&lt;li&gt;What is actually a good question?&lt;/li&gt;
&lt;li&gt;Who should be questioning it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not sure if hope is the world, but I am looking forward to the imperative speak tune reduce, and the question raise again.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>management</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From zero to working product in two hours</title>
      <dc:creator>Adam Shallcross</dc:creator>
      <pubDate>Fri, 15 May 2026 00:16:22 +0000</pubDate>
      <link>https://forem.com/adam_shallcross_562a6b01c/from-zero-to-working-product-in-two-hours-cgm</link>
      <guid>https://forem.com/adam_shallcross_562a6b01c/from-zero-to-working-product-in-two-hours-cgm</guid>
      <description>&lt;p&gt;So tonight I went to an Umbraco.AI hackathon, and somehow walked out with a working product.&lt;/p&gt;

&lt;p&gt;Not a hand-wavy prototype, not a "here's roughly what it could do" demo, but an actual end-to-end thing that does what it says on the tin, ready to show to people, ready to install on a real site.&lt;/p&gt;

&lt;p&gt;Two hours.&lt;/p&gt;

&lt;p&gt;And I didn't write a single line of code myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I picked
&lt;/h2&gt;

&lt;p&gt;AI-generated contact form spam has quietly become one of those headaches nobody really talks about. It reads as plausible English, has none of the obvious spam markers we've all trained ourselves to spot, and walks straight past the usual defences. If you run a public-facing website, you've almost certainly been quietly deleting it out of your inbox for the last twelve months, even if you hadn't quite put a name to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazq06m1nn9gsj019szke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazq06m1nn9gsj019szke.png" alt="Umbraco Form being rejected" width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nobody had built anything in the Umbraco ecosystem to deal with this at the contact form level, and that felt like a gap worth filling.&lt;/p&gt;

&lt;p&gt;The idea was simple enough. Every time someone submits a form on your website, send the contents to an AI model, ask it to score how likely the submission is to be genuine, and if it looks like spam, stop it before it ever reaches your inbox or your team. The legitimate enquiries flow through as normal. The spammy ones get held back and logged somewhere you can review them later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjkyw5ez2zxo8datlo7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjkyw5ez2zxo8datlo7p.png" alt="Flagged submissions that have been rejected and why" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Easy on paper. The challenge was getting it built, working, and demonstrable in the time available.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually built it
&lt;/h2&gt;

&lt;p&gt;I used Claude Code.&lt;/p&gt;

&lt;p&gt;For anyone who hasn't come across it yet, Claude Code is a tool that lets you describe what you want a piece of software to do, in plain English, and have it produce the actual code for you. You stay in the driving seat, you make the architectural decisions, you spot when it's gone off-track, you tell it what to fix. But the typing-the-actual-code part isn't your job anymore.&lt;/p&gt;

&lt;p&gt;That matters more than it sounds, because I'm not a developer. I've spent twenty-odd years in and around digital delivery, leading teams, scoping projects, understanding the shape of what good looks like, but I've never been the person hands-on-keyboard writing C# at one in the morning. Historically, to ship something like this, I would have needed to find a developer, brief them, wait for them, and hope I'd communicated clearly enough that what came back resembled what I'd asked for.&lt;/p&gt;

&lt;p&gt;Tonight, I just had the idea, the plan, and the tool.&lt;/p&gt;

&lt;p&gt;The plan was what made the difference, not the typing. I went in with a proper product brief, a rough hour-by-hour breakdown, and a small set of test examples I'd prepared covering obvious spam, plausible AI outreach, and genuine enquiries. Claude Code did the actual building. I did the thinking, the steering, and the testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;None of it went quite as I'd written it down.&lt;/p&gt;

&lt;p&gt;The biggest surprise was how much the world had moved on from the version of reality I was carrying in my head. Things I'd assumed worked one way had been changed, renamed, or replaced entirely in newer versions of the platform. There's something quite humbling about discovering all of that in real time, with a clock running and people drifting over to ask how it's going.&lt;/p&gt;

&lt;p&gt;This is where the tool genuinely earns its keep. When something didn't work the way the documentation said it should, I didn't have to dig through forum threads at midnight or guess at the right call to make. I'd describe the symptom, Claude Code would write a tiny test probe to confirm what was actually happening, and within a minute or two we'd know whether the real platform behaved like the docs said it did. Spoiler, it often didn't.&lt;/p&gt;

&lt;p&gt;That probably sounds obvious. It isn't. The temptation under time pressure is always to skip the small experiment and just start building the thing, and almost every time you regret it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built in
&lt;/h2&gt;

&lt;p&gt;The other thing I leaned into early was a safety net.&lt;/p&gt;

&lt;p&gt;A spam filter that breaks your contact form is worse than the spam itself, and I wanted that property baked in from the first commit rather than bolted on later. So the whole thing is wrapped in a "default-pass" guarantee. If the AI takes too long to respond, the submission goes through. If the response comes back garbled, the submission goes through. If anything at all goes wrong, the submission goes through.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjnz9ed1h7br2bptolli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjnz9ed1h7br2bptolli.png" alt="Terminal with rejection message" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The AI scoring is a quality-of-life feature, not a hard gate. That mindset shaped a lot of the design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftko972wdjc8kocpzbgse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftko972wdjc8kocpzbgse.png" alt="Umbraco Forms completed submissions" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also added a master kill switch and a configurable threshold, so anyone installing this can decide for themselves how aggressive they want it to be, or turn it off entirely without uninstalling. Small thing, but it matters. The worst thing a package can do is take options away from the people using it.&lt;/p&gt;

&lt;p&gt;None of those decisions were the tool's. They were mine. The tool just made them happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The headline lesson, for me, isn't really about spam filtering or contact forms. It's about what the role of the person sitting in front of these AI tools is actually becoming.&lt;/p&gt;

&lt;p&gt;You don't need to be able to write the code anymore. You need to be able to define the problem clearly, design the safety nets, spot when the output is heading in the wrong direction, and know when to stop and ask a better question. That's a different skill set to the one our industry has been hiring for the last fifteen years, and it's one that experienced engineers, product people, and consultants are already quietly really good at, even if they've never thought of themselves as builders.&lt;/p&gt;

&lt;p&gt;The gap between "I understand this conceptually" and "I can ship this in two hours" used to be made up of the typing. The actual writing of the code. The bit you had to either learn or pay someone else to do.&lt;/p&gt;

&lt;p&gt;That gap has narrowed, sharply.&lt;/p&gt;

&lt;p&gt;What's left in the gap is the harder, more interesting stuff. The judgement about what to build. The discipline to test your assumptions against reality. The instinct for when something is about to go wrong. The willingness to keep going when the first three attempts didn't work. None of that gets easier with better tools. If anything, it matters more, because the tools will happily produce something plausible and broken if you don't know what you're doing.&lt;/p&gt;

&lt;p&gt;The reason I got over the line tonight wasn't that I had Claude Code. It was that I knew exactly what I wanted to build, why, and what good looked like along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lands
&lt;/h2&gt;

&lt;p&gt;The package scored eight and a half out of nine on the test corpus. All the block decisions were correct. The legitimate enquiries flowed through normally, the spam got held back, and the flagged entries showed up in a dashboard a moment later for review.&lt;/p&gt;

&lt;p&gt;There's a clear roadmap of next steps already forming in my head. Editor feedback loops so you can mark a flagged entry as a false positive and have the model learn from it. Webhooks out to Slack so a flagged entry can ping a channel for review. Per-form custom prompts for sites running very different kinds of enquiries. Stats over time. None of that was going to fit in two hours, and that's fine, because the point of an evening like tonight isn't to finish the thing. It's to prove the thing is possible.&lt;/p&gt;

&lt;p&gt;The bit that's stayed with me, walking home, isn't the technical journey. It's that someone with no coding ability, but with twenty years of knowing what good software looks like, can now sit down in an evening and produce a real working product.&lt;/p&gt;

&lt;p&gt;That's a genuinely new thing in our industry, and I don't think most people have fully felt the shape of it yet.&lt;/p&gt;

&lt;p&gt;Two hours. One working product. Not a line of code typed by me.&lt;/p&gt;

&lt;p&gt;What would you point a two-hour hackathon at, if you knew you could actually finish it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nocode</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
