<?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: boop dev</title>
    <description>The latest articles on Forem by boop dev (@boop_one).</description>
    <link>https://forem.com/boop_one</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3712963%2F0e029887-a274-49fc-8d93-4f1a28f19b28.png</url>
      <title>Forem: boop dev</title>
      <link>https://forem.com/boop_one</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/boop_one"/>
    <language>en</language>
    <item>
      <title>Passkeys in Production: Adding WebAuthn to a SaaS</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Sun, 18 Jan 2026 12:39:07 +0000</pubDate>
      <link>https://forem.com/boop_one/passkeys-in-production-adding-webauthn-to-a-saas-4eo9</link>
      <guid>https://forem.com/boop_one/passkeys-in-production-adding-webauthn-to-a-saas-4eo9</guid>
      <description>&lt;h1&gt;
  
  
  Passkeys in Production: Adding WebAuthn to a SaaS
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Passwordless authentication with Face ID, Touch ID, and security keys&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Passwords are a liability. Users reuse them. They get phished. They forget them and reset them constantly.&lt;/p&gt;

&lt;p&gt;Passkeys are the future: cryptographic credentials stored on your device, unlocked with biometrics. No password to steal. No password to forget.&lt;/p&gt;

&lt;p&gt;Here's how we added passkey support to Boop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Passkeys?
&lt;/h2&gt;

&lt;p&gt;Passkeys use the WebAuthn standard. Instead of a password, your device stores a cryptographic key pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private key&lt;/strong&gt;: Stays on your device, never leaves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public key&lt;/strong&gt;: Stored on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To log in, your device signs a challenge with the private key. The server verifies the signature with the public key. No shared secrets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐                      ┌─────────────┐
│   Device    │                      │   Server    │
│             │                      │             │
│  Private ───┼── Signs challenge ──▶│   Public    │
│    Key      │                      │    Key      │
│             │◀── Verified! ────────┼─            │
└─────────────┘                      └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;We use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;@simplewebauthn/browser&lt;/strong&gt; - Client-side WebAuthn API wrapper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@simplewebauthn/server&lt;/strong&gt; - Server-side verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; - Temporary challenge storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma&lt;/strong&gt; - Passkey credential storage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Registration Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Generate Registration Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GET /api/passkey/register/options&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="nx"&gt;session&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;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&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="nx"&gt;prisma&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="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;session&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="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passkeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&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;generateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;rpName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boop.one&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rpID&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;URL&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;userName&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="na"&gt;userDisplayName&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;name&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Don't require attestation&lt;/span&gt;
    &lt;span class="na"&gt;attestationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Prevent re-registering existing passkeys&lt;/span&gt;
    &lt;span class="na"&gt;excludeCredentials&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;passkeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pk&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="c1"&gt;// Prefer platform authenticators (Face ID, Touch ID)&lt;/span&gt;
    &lt;span class="na"&gt;authenticatorSelection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;residentKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preferred&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preferred&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;span class="c1"&gt;// Store challenge for verification&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeWebAuthnChallenge&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="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenge&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="nx"&gt;options&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;h3&gt;
  
  
  Step 2: Browser Creates Credential
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client-side&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;startRegistration&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;@simplewebauthn/browser&lt;/span&gt;&lt;span class="dl"&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;optionsRes&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/passkey/register/options&lt;/span&gt;&lt;span class="dl"&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;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;optionsRes&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="c1"&gt;// This triggers Face ID / Touch ID / Security Key prompt&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&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;startRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Send credential to server&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/passkey/register/verify&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MacBook Pro&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Verify and Store
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/passkey/register/verify&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;POST&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;Request&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;credential&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Retrieve the challenge from Redis&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedChallenge&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;getAndDeleteWebAuthnChallenge&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="c1"&gt;// Verify the registration&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verification&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;verifyRegistrationResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedOrigin&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedRPID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rpID&lt;/span&gt;&lt;span class="p"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&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;Verification failed&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;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Store the passkey&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;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;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&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;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialDeviceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;backedUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialBackedUp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&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="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;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Database Schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Passkey {
  id           String   @id @default(cuid())
  userId       String
  credentialId String   @unique
  publicKey    String   // Base64url encoded
  counter      Int      // Replay attack protection
  deviceType   String   // "singleDevice" or "multiDevice"
  backedUp     Boolean  // Is credential backed up (iCloud, etc)?
  transports   String[] // ["internal", "usb", "ble", "nfc"]
  name         String?  // User-friendly name
  createdAt    DateTime @default(now())
  lastUsedAt   DateTime?

  user User @relation(fields: [userId], references: [id])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Challenge Storage in Redis
&lt;/h2&gt;

&lt;p&gt;Challenges are one-time use with a 5-minute TTL:&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="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;storeWebAuthnChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&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="nx"&gt;challenge&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`webauthn:challenge:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;  &lt;span class="c1"&gt;// 5 minute expiry&lt;/span&gt;
  &lt;span class="p"&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;getAndDeleteWebAuthnChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`webauthn:challenge:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// One-time use&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;challenge&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;h2&gt;
  
  
  Authentication Flow
&lt;/h2&gt;

&lt;p&gt;Similar to registration, but uses &lt;code&gt;startAuthentication&lt;/code&gt; instead:&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;// Client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/passkey/auth/options&lt;/span&gt;&lt;span class="dl"&gt;"&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;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assertion&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;startAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/passkey/auth/verify&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;assertion&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;h2&gt;
  
  
  Counter Verification (Replay Protection)
&lt;/h2&gt;

&lt;p&gt;Each passkey has a counter that increments with every use. If we see a lower counter than expected, the credential may have been cloned:&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;// During authentication verification&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;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCounter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;storedCounter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Possible credential cloning attack!&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;Counter too low - possible replay attack&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="c1"&gt;// Update stored counter&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;passkey&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCounter&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;h2&gt;
  
  
  User Experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Managing Passkeys
&lt;/h3&gt;

&lt;p&gt;Users can register multiple passkeys (one per device) and name them:&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;passkeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;passkey&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;platform&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="s2"&gt;📱&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="s2"&gt;🔑&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unnamed Passkey&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Added &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&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;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;deletePasskey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passkey&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Remove&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Device Type Icons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📱 Platform authenticator (Face ID, Touch ID, Windows Hello)&lt;/li&gt;
&lt;li&gt;🔑 Cross-platform (USB security key, NFC)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tracking Last Used
&lt;/h3&gt;

&lt;p&gt;Update &lt;code&gt;lastUsedAt&lt;/code&gt; on every successful authentication:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;passkey&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastUsedAt&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;Date&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;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Origin Validation
&lt;/h3&gt;

&lt;p&gt;Always verify the origin matches your domain:&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="nx"&gt;expectedOrigin&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;expectedRPID&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;URL&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. No Attestation Required
&lt;/h3&gt;

&lt;p&gt;We use &lt;code&gt;attestationType: "none"&lt;/code&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attestation adds friction&lt;/li&gt;
&lt;li&gt;We don't need to verify the authenticator model&lt;/li&gt;
&lt;li&gt;Privacy: attestation can fingerprint devices&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Challenge Expiry
&lt;/h3&gt;

&lt;p&gt;Challenges expire after 5 minutes and are single-use (delete after retrieval).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multiple Passkeys
&lt;/h3&gt;

&lt;p&gt;Users should register passkeys on multiple devices. If they lose their phone, they can still log in with their laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback: Traditional Auth
&lt;/h2&gt;

&lt;p&gt;Passkeys are additive. Users can still log in with email/password if they haven't set up passkeys. We show passkey login first if they have any registered:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasPasskeys&lt;/span&gt; &lt;span class="o"&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;passkeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;hasPasskeys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Show "Sign in with Passkey" button first&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Show email/password form&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;Since adding passkeys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;50%&lt;/strong&gt; of active users have registered at least one passkey&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero&lt;/strong&gt; password-related support tickets from passkey users&lt;/li&gt;
&lt;li&gt;Login is &lt;strong&gt;faster&lt;/strong&gt; (biometric vs typing password)&lt;/li&gt;
&lt;li&gt;Phishing-&lt;strong&gt;proof&lt;/strong&gt; (passkeys are origin-bound)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a library&lt;/strong&gt; - Don't implement WebAuthn from scratch. SimpleWebAuthn handles the edge cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Store challenges in Redis&lt;/strong&gt; - They need to be ephemeral and accessible across requests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Track device type&lt;/strong&gt; - Users want to know which passkeys are on which devices&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allow multiple passkeys&lt;/strong&gt; - One per device is the expected pattern&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep password fallback&lt;/strong&gt; - Not everyone has a compatible device&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Passkeys are the best authentication UX that also happens to be the most secure. Your users just need to look at their phone or touch their laptop.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Passwordless authentication at &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webauthn</category>
      <category>authentication</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How We Monitor Internal Services Without Opening Firewall Ports</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Sat, 17 Jan 2026 11:31:43 +0000</pubDate>
      <link>https://forem.com/boop_one/how-we-monitor-internal-services-without-opening-firewall-ports-362j</link>
      <guid>https://forem.com/boop_one/how-we-monitor-internal-services-without-opening-firewall-ports-362j</guid>
      <description>&lt;h1&gt;
  
  
  How We Monitor Internal Services Without Opening Firewall Ports
&lt;/h1&gt;

&lt;p&gt;Most uptime monitors only watch what's publicly accessible. But your database, internal APIs, and microservices? Those stay invisible until something breaks and a user complains.&lt;/p&gt;

&lt;p&gt;We built private agents to solve this. Here's how they work and why they're different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Monitoring Internal Services
&lt;/h2&gt;

&lt;p&gt;Your internal infrastructure is behind a firewall for good reason. But that creates a monitoring blind spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Databases&lt;/strong&gt; - Is your PostgreSQL replica in sync?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal APIs&lt;/strong&gt; - Is your auth service responding?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservices&lt;/strong&gt; - Is that background worker healthy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private networks&lt;/strong&gt; - Are your VPC endpoints working?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional solutions either require:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exposing services to the internet (security nightmare)&lt;/li&gt;
&lt;li&gt;Expensive enterprise monitoring ($95+/month per agent)&lt;/li&gt;
&lt;li&gt;Self-hosting complex monitoring stacks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We wanted something simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Private Agents Work
&lt;/h2&gt;

&lt;p&gt;A private agent is a lightweight Docker container that runs inside your network. It monitors your internal services and reports back to Boop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                  Your Network                        │
│                                                      │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐        │
│   │Database │    │ API     │    │ Service │        │
│   │ :5432   │    │ :8080   │    │ :3000   │        │
│   └────┬────┘    └────┬────┘    └────┬────┘        │
│        │              │              │              │
│        └──────────────┼──────────────┘              │
│                       │                             │
│                ┌──────┴──────┐                      │
│                │   Boop      │                      │
│                │   Agent     │                      │
│                └──────┬──────┘                      │
│                       │                             │
└───────────────────────┼─────────────────────────────┘
                        │ HTTPS only (outbound)
                        ▼
                 ┌─────────────┐
                 │  boop.one   │
                 └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight: outbound only.&lt;/strong&gt; The agent connects to Boop - Boop never connects to you. No inbound firewall rules. No VPN tunnels. No exposed ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in 60 Seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an agent in your Boop dashboard&lt;/li&gt;
&lt;li&gt;Copy the setup token (valid for 24 hours)&lt;/li&gt;
&lt;li&gt;Run the container:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; boop-agent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;BOOP_SETUP_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_token_here &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; boop-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  boopone/agent:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The agent automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exchanges the setup token for a secure active token&lt;/li&gt;
&lt;li&gt;Pulls monitor configurations from Boop&lt;/li&gt;
&lt;li&gt;Starts checking your internal services&lt;/li&gt;
&lt;li&gt;Reports results back to your dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Buffer: Never Lose Data
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. What happens when your network has an outage? Or the agent temporarily loses connectivity to Boop?&lt;/p&gt;

&lt;p&gt;Most monitoring tools just... lose that data. You get gaps in your graphs and no idea what happened.&lt;/p&gt;

&lt;p&gt;We built a buffer into the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Buffer Works
&lt;/h3&gt;

&lt;p&gt;When the agent can't reach Boop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It keeps running checks using cached configuration&lt;/li&gt;
&lt;li&gt;Results are stored locally in a persistent buffer&lt;/li&gt;
&lt;li&gt;Each result gets a timestamp and unique ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When connectivity returns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent submits all buffered results&lt;/li&gt;
&lt;li&gt;Boop processes them with their original timestamps&lt;/li&gt;
&lt;li&gt;Your monitoring history stays complete
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeline:
────────────────────────────────────────────────────►

[Normal]     [Disconnected]     [Reconnected]
   │              │                   │
   │   Agent      │   Agent buffers   │   Buffer syncs
   │   reports    │   results locally │   to Boop
   │   live       │                   │
   ▼              ▼                   ▼

   📊             💾                  📊
   Live data      Stored locally      Historical data
                  (up to 10,000       restored
                   results)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Smart Alert Handling
&lt;/h3&gt;

&lt;p&gt;Here's the clever part: buffered results don't trigger alerts.&lt;/p&gt;

&lt;p&gt;Think about it - if your agent was offline for 30 minutes and buffered 50 check results, you don't want 50 alert notifications flooding your Slack when it reconnects. Those results are historical. The situation has either resolved itself or you've already noticed.&lt;/p&gt;

&lt;p&gt;So Boop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Records all buffered results for your historical data&lt;/li&gt;
&lt;li&gt;Updates your uptime graphs accurately&lt;/li&gt;
&lt;li&gt;Does NOT fire alerts for old data&lt;/li&gt;
&lt;li&gt;Marks them as "buffered" so you know what happened&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Makes This Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Price
&lt;/h3&gt;

&lt;p&gt;Most competitors charge $95/month or more for a single private agent. We start at $7/month.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost per Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Competitors&lt;/td&gt;
&lt;td&gt;$95+/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boop&lt;/td&gt;
&lt;td&gt;$7/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 93% cheaper. Not a typo.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Security Model
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Setup tokens expire in 24 hours (one-time use)&lt;/li&gt;
&lt;li&gt;Active tokens auto-rotate every 7 days&lt;/li&gt;
&lt;li&gt;All communication is outbound HTTPS only&lt;/li&gt;
&lt;li&gt;No credentials stored on the agent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Full Feature Parity
&lt;/h3&gt;

&lt;p&gt;Private agents support everything public monitoring does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP/HTTPS checks with status code and keyword validation&lt;/li&gt;
&lt;li&gt;DNS lookups&lt;/li&gt;
&lt;li&gt;TCP port checks&lt;/li&gt;
&lt;li&gt;SSL certificate monitoring&lt;/li&gt;
&lt;li&gt;Custom headers and authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Resilience
&lt;/h3&gt;

&lt;p&gt;The buffer means your agent survives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network blips&lt;/li&gt;
&lt;li&gt;DNS issues&lt;/li&gt;
&lt;li&gt;Temporary firewall problems&lt;/li&gt;
&lt;li&gt;Boop maintenance windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You never lose monitoring data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Startup with hybrid infrastructure:&lt;/strong&gt;&lt;br&gt;
Monitor your AWS RDS database, internal auth API, and Redis cache - all from one agent, one dashboard, one alerting pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agency managing client servers:&lt;/strong&gt;&lt;br&gt;
Drop an agent on each client's network. Monitor their internal services without requiring them to expose anything to the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise with compliance requirements:&lt;/strong&gt;&lt;br&gt;
Keep sensitive infrastructure completely internal while still getting modern monitoring and alerting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Private agents are available on all paid plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starter ($7): 1 agent&lt;/li&gt;
&lt;li&gt;Hobbyist ($15): 2 agents&lt;/li&gt;
&lt;li&gt;Professional ($79): 10 agents&lt;/li&gt;
&lt;li&gt;Enterprise plans: 25+ agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set one up at &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;boop.one&lt;/a&gt;. The agent is open source if you want to see exactly what it does before deploying.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about monitoring internal infrastructure? I'm happy to help - drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>monitoring</category>
      <category>security</category>
    </item>
    <item>
      <title>Why We Made Our Monitoring Stats Public</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Fri, 16 Jan 2026 20:05:20 +0000</pubDate>
      <link>https://forem.com/boop_one/why-we-made-our-monitoring-stats-public-3beo</link>
      <guid>https://forem.com/boop_one/why-we-made-our-monitoring-stats-public-3beo</guid>
      <description>&lt;h1&gt;
  
  
  Why We Made Our Monitoring Stats Public
&lt;/h1&gt;

&lt;p&gt;Most monitoring services hide their numbers. We decided to do the opposite.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://boop.one/live" rel="noopener noreferrer"&gt;boop.one/live&lt;/a&gt;, you can see exactly how Boop is performing right now - checks per minute, regional latency, success rates, everything. No login required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can See
&lt;/h2&gt;

&lt;p&gt;The live dashboard shows real-time stats from our monitoring infrastructure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global Numbers&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks per minute (currently ~150)&lt;/li&gt;
&lt;li&gt;Total checks in the last 24 hours (330,000+)&lt;/li&gt;
&lt;li&gt;Active monitors across the platform&lt;/li&gt;
&lt;li&gt;Incidents detected in the last 24 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Regional Performance&lt;/strong&gt;&lt;br&gt;
We run checks from 4 regions, and you can see each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;US-East (Virginia)&lt;/li&gt;
&lt;li&gt;US-West (Los Angeles)&lt;/li&gt;
&lt;li&gt;EU-Central (Frankfurt)&lt;/li&gt;
&lt;li&gt;Asia-Pacific (Singapore)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each region, we show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of checks performed&lt;/li&gt;
&lt;li&gt;Average response time&lt;/li&gt;
&lt;li&gt;Success rate percentage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additional Metrics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSL certificates being monitored&lt;/li&gt;
&lt;li&gt;Average days until certificate expiration&lt;/li&gt;
&lt;li&gt;DNS success rates&lt;/li&gt;
&lt;li&gt;Platform uptime percentage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Make This Public?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. We Should Practice What We Preach
&lt;/h3&gt;

&lt;p&gt;We are a monitoring company. If we are going to tell you that uptime matters, we should be willing to show ours.&lt;/p&gt;

&lt;p&gt;Hiding our stats while asking you to trust us with yours felt hypocritical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It Demonstrates Scale
&lt;/h3&gt;

&lt;p&gt;Anyone can claim they handle "millions of checks." Showing real numbers proves it.&lt;/p&gt;

&lt;p&gt;When you see 330,000+ checks in the last 24 hours updating in real-time, you know it is not marketing fluff.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It Builds Trust
&lt;/h3&gt;

&lt;p&gt;Before you sign up for a monitoring service, you probably want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it actually reliable?&lt;/li&gt;
&lt;li&gt;Does it work globally?&lt;/li&gt;
&lt;li&gt;Can it handle scale?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our live page answers all of these without you having to trust our marketing copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It Keeps Us Honest
&lt;/h3&gt;

&lt;p&gt;When your stats are public, you cannot hide problems. If our success rate drops, everyone sees it.&lt;/p&gt;

&lt;p&gt;This creates internal pressure to maintain quality. We are not just accountable to paying customers - we are accountable to anyone who visits the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Side
&lt;/h2&gt;

&lt;p&gt;The live stats page pulls from a public API endpoint that aggregates data from our monitoring infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check counts come from our job queue metrics&lt;/li&gt;
&lt;li&gt;Response times are averaged from recent check results&lt;/li&gt;
&lt;li&gt;Success rates are calculated from the last hour of data&lt;/li&gt;
&lt;li&gt;Regional breakdowns use our multi-region worker data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page refreshes every 15 seconds, so you are seeing near-real-time data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;Publishing our stats publicly changed how we think about reliability. When a metric looks bad on the dashboard, we feel it immediately.&lt;/p&gt;

&lt;p&gt;It also became an unexpected marketing tool. Developers appreciate transparency, and the live page has driven signups from people who just wanted to see what we were about.&lt;/p&gt;

&lt;h2&gt;
  
  
  See It Yourself
&lt;/h2&gt;

&lt;p&gt;Check out the live dashboard: &lt;a href="https://boop.one/live" rel="noopener noreferrer"&gt;boop.one/live&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the numbers update in real-time. See which regions are fastest. Notice how many checks we are running right now.&lt;/p&gt;

&lt;p&gt;And if you want that kind of visibility for your own infrastructure, &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt; includes public status pages so you can give your users the same transparency.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What metrics would you want to see from a monitoring service? Drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>webdev</category>
      <category>transparency</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Use Claude to Watch My Infrastructure While I Sleep</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Thu, 15 Jan 2026 16:32:55 +0000</pubDate>
      <link>https://forem.com/boop_one/how-i-use-claude-to-watch-my-infrastructure-while-i-sleep-3fcf</link>
      <guid>https://forem.com/boop_one/how-i-use-claude-to-watch-my-infrastructure-while-i-sleep-3fcf</guid>
      <description>&lt;h1&gt;
  
  
  How I Use Claude to Watch My Infrastructure While I Sleep
&lt;/h1&gt;

&lt;p&gt;Running a monitoring service means your infrastructure can't go down. The irony isn't lost on me - if &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt; goes offline, nobody gets alerted that their own services are down.&lt;/p&gt;

&lt;p&gt;Our customers rely on us to watch their websites, APIs, and servers 24/7. When their infrastructure has a problem at 3am, we're the ones who wake them up. That only works if we're awake ourselves.&lt;/p&gt;

&lt;p&gt;So I built something a little unusual: I have Claude running every 30 minutes, checking on everything, and fixing problems automatically. Here's how it works and why I can actually sleep at night.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Alert Fatigue vs. Missing Real Issues
&lt;/h2&gt;

&lt;p&gt;Traditional monitoring has two failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Too many alerts&lt;/strong&gt; - You get paged for every blip, eventually ignore them all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too few alerts&lt;/strong&gt; - You miss the one that matters until a user complains&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted something smarter. Not just "send me an alert" but "diagnose the problem and fix it if you can."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: Claude as an On-Call Engineer
&lt;/h2&gt;

&lt;p&gt;Every 30 minutes, a cron job runs a health check script. But instead of just checking metrics and sending alerts, it hands off to Claude with a simple mission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check everything. If something's broken, fix it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what Claude actually does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checks Fly.io machine status&lt;/strong&gt; - Are all 4 regions running?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hits the health endpoint&lt;/strong&gt; - Is the API responding?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviews recent logs&lt;/strong&gt; - Any errors or warnings?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks queue depth&lt;/strong&gt; - Is work backing up?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnoses issues&lt;/strong&gt; - What's actually wrong?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixes what it can&lt;/strong&gt; - Restart machines, deploy fixes, push code changes
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Do a health check on boop infrastructure..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Bash,Read,Edit,Write,Grep,Glob"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; bypassPermissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Claude Can Actually Fix
&lt;/h2&gt;

&lt;p&gt;When Claude finds a problem, it doesn't just report it. It acts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine down?&lt;/strong&gt; Restart it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fly machine start &amp;lt;machine-id&amp;gt; &lt;span class="nt"&gt;-a&lt;/span&gt; boop-monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Need a redeploy?&lt;/strong&gt; Deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fly deploy &lt;span class="nt"&gt;-a&lt;/span&gt; boop-monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code bug causing issues?&lt;/strong&gt; Fix and push:&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;# Edit the problematic code&lt;/span&gt;
&lt;span class="c"&gt;# Run npm run build to verify&lt;/span&gt;
git add &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Auto-fix: improve queue handling"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CI/CD pipeline automatically deploys code changes. I wake up to an email saying "Fixed a bug while you slept."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contention Problem
&lt;/h2&gt;

&lt;p&gt;Here's where it gets tricky. What happens when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I'm actively coding and have uncommitted changes?&lt;/li&gt;
&lt;li&gt;A GitHub Action is deploying?&lt;/li&gt;
&lt;li&gt;Another health check is already running?&lt;/li&gt;
&lt;li&gt;I'm doing maintenance and don't want automation interfering?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Claude just blindly runs and pushes code, it could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overwrite my work in progress&lt;/li&gt;
&lt;li&gt;Race against CI/CD and cause conflicts&lt;/li&gt;
&lt;li&gt;Stack multiple fix attempts on top of each other&lt;/li&gt;
&lt;li&gt;Push broken code because it didn't have the full context&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Four Layers of Contention Prevention
&lt;/h2&gt;

&lt;p&gt;I built a "do not disturb" system with four layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Lock File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LOCK_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/boop-health-check.lock"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCK_FILE&lt;/span&gt;&lt;span class="s2"&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;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SKIP - Another health check is already running"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCK_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one health check runs at a time. Period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Uncommitted Changes Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SKIP - Uncommitted changes detected in main repo"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I'm working on something, Claude backs off. My uncommitted changes are a signal that a human is actively developing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: CI/CD Awareness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUNNING_ACTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh run list &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--status&lt;/span&gt; in_progress &lt;span class="nt"&gt;--json&lt;/span&gt; databaseId &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'length'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUNNING_ACTIONS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SKIP - &lt;/span&gt;&lt;span class="nv"&gt;$RUNNING_ACTIONS&lt;/span&gt;&lt;span class="s2"&gt; GitHub Action(s) in progress"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If GitHub Actions is deploying, Claude waits. No racing against the pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Manual Override
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAIN_REPO&lt;/span&gt;&lt;span class="s2"&gt;/.no-health-check"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SKIP - Do not disturb file present"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I'm doing something unusual and want automation to stay away, I create a &lt;code&gt;.no-health-check&lt;/code&gt; file. Simple human override.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 30 minutes:
┌─────────────────────────────────────────┐
│ Health Check Script Starts              │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────┐
│ Check: Lock file exists?                │──Yes──▶ EXIT (another check running)
└────────────────┬────────────────────────┘
                 │ No
                 ▼
┌─────────────────────────────────────────┐
│ Check: .no-health-check file exists?    │──Yes──▶ EXIT (manual override)
└────────────────┬────────────────────────┘
                 │ No
                 ▼
┌─────────────────────────────────────────┐
│ Check: Uncommitted changes in repo?     │──Yes──▶ EXIT (human working)
└────────────────┬────────────────────────┘
                 │ No
                 ▼
┌─────────────────────────────────────────┐
│ Check: GitHub Actions running?          │──Yes──▶ EXIT (CI/CD in progress)
└────────────────┬────────────────────────┘
                 │ No
                 ▼
┌─────────────────────────────────────────┐
│ Run pre-checks (machines, API, queue)   │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────┐
│ Launch Claude with context              │
│ - Check status                          │
│ - Review logs                           │
│ - Diagnose issues                       │
│ - Fix if possible                       │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────┐
│ If fixes made → Email notification      │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bounded Autonomy
&lt;/h2&gt;

&lt;p&gt;Claude isn't running wild. It has explicit boundaries:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read files and logs&lt;/li&gt;
&lt;li&gt;Run bash commands (fly status, curl, etc.)&lt;/li&gt;
&lt;li&gt;Edit code files&lt;/li&gt;
&lt;li&gt;Grep/search the codebase&lt;/li&gt;
&lt;li&gt;Commit and push to git&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cannot do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete files&lt;/li&gt;
&lt;li&gt;Access cloud provider directly (only via fly CLI)&lt;/li&gt;
&lt;li&gt;Make speculative improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt explicitly says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Only make code changes if there's a clear issue that needs fixing. Do NOT make speculative improvements or refactors."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Notifications: Only When It Matters
&lt;/h2&gt;

&lt;p&gt;I don't get emailed for every health check. Only when something actually happened:&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="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiE&lt;/span&gt; &lt;span class="s2"&gt;"(fixed|restarted|deployed|resolved|corrected|pushed to git)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;send_email &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL_SUBJECT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL_BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is healthy, the log just says "All systems healthy" and I never hear about it.&lt;/p&gt;

&lt;p&gt;If Claude fixed something, I get an email with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the pre-check found&lt;/li&gt;
&lt;li&gt;What Claude did to fix it&lt;/li&gt;
&lt;li&gt;Full output for review&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Results
&lt;/h2&gt;

&lt;p&gt;In the past month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;48 health checks per day&lt;/strong&gt; (every 30 minutes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~1,400 total checks&lt;/strong&gt; run automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 automated fixes&lt;/strong&gt; pushed while I slept&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0 false positives&lt;/strong&gt; or unnecessary alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0 conflicts&lt;/strong&gt; with my development work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixes were real issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A worker that crashed and needed restart&lt;/li&gt;
&lt;li&gt;A connection pool that needed tuning&lt;/li&gt;
&lt;li&gt;A queue backpressure threshold that needed adjustment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each time, I woke up to an email explaining what happened and what was fixed. Reviewed the changes, confirmed they were correct, moved on with my day.&lt;/p&gt;

&lt;p&gt;Most importantly: I sleep well. I don't lie awake wondering if something's broken. I don't compulsively check my phone at 2am. I know that if something goes wrong, Claude will fix it and that lets me actually rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works for Solo Developers
&lt;/h2&gt;

&lt;p&gt;If you're running infrastructure solo, you can't be on-call 24/7. You have to sleep. You have other things to do.&lt;/p&gt;

&lt;p&gt;This approach gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coverage&lt;/strong&gt; - Something is watching even when I'm not&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligence&lt;/strong&gt; - Not just alerts, but diagnosis and fixes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt; - Multiple layers prevent automation from causing problems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency&lt;/strong&gt; - I know exactly what happened and why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The contention strategy is the key. Without it, I'd be afraid to let automation touch anything. With it, I know Claude will back off whenever there's a reason to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;This pattern works with any infrastructure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a health check script that does your pre-flight checks&lt;/li&gt;
&lt;li&gt;Add contention prevention (lock file, git status, CI check)&lt;/li&gt;
&lt;li&gt;Call Claude with bounded permissions&lt;/li&gt;
&lt;li&gt;Only notify on actual fixes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Claude CLI makes this straightforward. The &lt;code&gt;--allowedTools&lt;/code&gt; flag lets you restrict what Claude can do, and &lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt; lets it run unattended.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running a service solo? I'd love to hear how you handle on-call. Drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>automation</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>6 Free Developer Tools You Can Use Right Now (No Signup Required)</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Thu, 15 Jan 2026 15:52:53 +0000</pubDate>
      <link>https://forem.com/boop_one/6-free-developer-tools-you-can-use-right-now-no-signup-required-3med</link>
      <guid>https://forem.com/boop_one/6-free-developer-tools-you-can-use-right-now-no-signup-required-3med</guid>
      <description>&lt;h1&gt;
  
  
  6 Free Developer Tools You Can Use Right Now (No Signup Required)
&lt;/h1&gt;

&lt;p&gt;Sometimes you just need a quick tool to check something. Is my SSL certificate expiring? What DNS records does this domain have? Is that port open?&lt;/p&gt;

&lt;p&gt;I built these tools for &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt; because I kept needing them myself. They are completely free, no signup required, and work right in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SSL Certificate Checker
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/ssl-checker" rel="noopener noreferrer"&gt;boop.one/tools/ssl-checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check any domain's SSL certificate instantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Certificate expiration date&lt;/li&gt;
&lt;li&gt;Certificate chain validation&lt;/li&gt;
&lt;li&gt;Issuer information&lt;/li&gt;
&lt;li&gt;Days until expiry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Quick check before your cert expires and Chrome starts showing scary warnings to your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DNS Lookup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/dns-lookup" rel="noopener noreferrer"&gt;boop.one/tools/dns-lookup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Query DNS records for any domain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A, AAAA, CNAME, MX, TXT, NS records&lt;/li&gt;
&lt;li&gt;Multiple record type support&lt;/li&gt;
&lt;li&gt;Raw DNS response data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Debugging DNS propagation, checking MX records before setting up email, verifying TXT records for domain verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. HTTP Headers Checker
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/http-headers" rel="noopener noreferrer"&gt;boop.one/tools/http-headers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See exactly what headers a URL returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response status code&lt;/li&gt;
&lt;li&gt;All response headers&lt;/li&gt;
&lt;li&gt;Security headers check&lt;/li&gt;
&lt;li&gt;Cache control settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Debugging CORS issues, checking security headers, verifying cache settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. WHOIS Lookup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/whois-lookup" rel="noopener noreferrer"&gt;boop.one/tools/whois-lookup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get domain registration information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registrar details&lt;/li&gt;
&lt;li&gt;Registration and expiry dates&lt;/li&gt;
&lt;li&gt;Nameserver information&lt;/li&gt;
&lt;li&gt;Domain status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Checking when a domain expires, finding out who owns a domain, verifying domain transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Ping Tool
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/ping" rel="noopener noreferrer"&gt;boop.one/tools/ping&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test if a host is reachable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response time measurement&lt;/li&gt;
&lt;li&gt;Packet loss detection&lt;/li&gt;
&lt;li&gt;Multiple ping support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Quick connectivity check, latency testing, verifying a server is up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Port Checker
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://boop.one/tools/port-checker" rel="noopener noreferrer"&gt;boop.one/tools/port-checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check if specific ports are open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TCP port scanning&lt;/li&gt;
&lt;li&gt;Common port presets&lt;/li&gt;
&lt;li&gt;Custom port input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Verifying firewall rules, checking if services are exposed, debugging connection issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Free?
&lt;/h2&gt;

&lt;p&gt;Honestly? These tools bring people to Boop. Some of them end up signing up for monitoring (which is what we actually sell). But even if you never sign up, these tools are genuinely useful and I wanted them to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need Ongoing Monitoring?
&lt;/h2&gt;

&lt;p&gt;If you find yourself checking these things repeatedly, you might want automated monitoring instead. &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt; monitors your sites every 30 seconds from 4 global regions and alerts you when something goes wrong.&lt;/p&gt;

&lt;p&gt;Free tier includes 10 monitors. No credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What other tools would be useful? Drop a comment - I might build it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Monitor Your Side Project in 5 Minutes</title>
      <dc:creator>boop dev</dc:creator>
      <pubDate>Thu, 15 Jan 2026 15:14:33 +0000</pubDate>
      <link>https://forem.com/boop_one/monitor-your-side-project-in-5-minutes-1c47</link>
      <guid>https://forem.com/boop_one/monitor-your-side-project-in-5-minutes-1c47</guid>
      <description>&lt;h1&gt;
  
  
  Monitor Your Side Project in 5 Minutes
&lt;/h1&gt;

&lt;p&gt;Your side project is finally live. You shipped it. You're proud of it. But here's an uncomfortable question: &lt;strong&gt;how will you know when it goes down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer, unfortunately, is usually "when an angry user tweets at me" or "when I notice it 3 days later." I've been there. It sucks.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Boop&lt;/a&gt; specifically because I got tired of finding out my projects were down from someone else. Now I'm going to show you how to set up monitoring in literally 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother Monitoring a Side Project?
&lt;/h2&gt;

&lt;p&gt;Look, I get it. It's a side project. You're not running AWS here. But consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users won't tell you&lt;/strong&gt; - Most will just leave and never come back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search engines notice&lt;/strong&gt; - Downtime can tank your SEO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL certificates expire&lt;/strong&gt; - And you'll forget until Chrome shows that scary warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It takes 5 minutes&lt;/strong&gt; - And Boop has a free tier, so there's zero excuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 5-Minute Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Sign Up (30 seconds)
&lt;/h3&gt;

&lt;p&gt;Head to &lt;a href="https://boop.one" rel="noopener noreferrer"&gt;boop.one&lt;/a&gt; and create an account. No credit card required for the free tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add Your First Monitor (1 minute)
&lt;/h3&gt;

&lt;p&gt;Click "Add Monitor" and enter your URL. That's it. Boop will automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check if your site is up every 30 seconds&lt;/li&gt;
&lt;li&gt;Monitor from 4 global regions (US East, US West, Europe, Asia)&lt;/li&gt;
&lt;li&gt;Track response times&lt;/li&gt;
&lt;li&gt;Check your SSL certificate expiration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Set Up Alerts (2 minutes)
&lt;/h3&gt;

&lt;p&gt;This is the important part. Go to Settings and connect your preferred notification channel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt; - Classic, always works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; - Great if you live in Slack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discord&lt;/strong&gt; - Perfect for indie hackers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; - For the automation nerds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I personally use Discord because I have a private server for all my project notifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: (Optional) Create a Status Page (1 minute)
&lt;/h3&gt;

&lt;p&gt;Want to look professional? Create a public status page for your users. It takes one click and you get a URL like &lt;code&gt;status.yourproject.com&lt;/code&gt; that shows real-time uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;p&gt;With just those 5 minutes, you now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30-second checks&lt;/strong&gt; from 4 continents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant alerts&lt;/strong&gt; when something breaks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL expiry warnings&lt;/strong&gt; before they become emergencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time tracking&lt;/strong&gt; to catch slowdowns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A status page&lt;/strong&gt; to show users (and yourself) that you're serious&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Peace of Mind Factor
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect: &lt;strong&gt;the mental relief&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before I had monitoring, there was always this low-grade anxiety. "Is my project up? Should I check? When was the last time I checked?"&lt;/p&gt;

&lt;p&gt;Now I just... don't think about it. If something's wrong, I'll know within 30 seconds. If I don't get an alert, everything's fine. It's genuinely freeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;Look, monitoring isn't sexy. It's not a feature you can show off. But 5 minutes now saves you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embarrassing "is it just me or is the site down?" messages&lt;/li&gt;
&lt;li&gt;Lost users who silently bounced&lt;/li&gt;
&lt;li&gt;The panic of discovering a 12-hour outage&lt;/li&gt;
&lt;li&gt;Expired SSL certificates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://boop.one" rel="noopener noreferrer"&gt;Set up monitoring for your side project&lt;/a&gt; - your future self will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building Boop as an indie hacker. If you have feedback or feature requests, I'd love to hear them in the comments or on &lt;a href="https://twitter.com/boopmonitoring" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
