<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Spicy</title>
    <description>The latest articles on DEV Community by Spicy (@spicykim).</description>
    <link>https://dev.to/spicykim</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3930300%2Ff80e2e97-ebe7-4ee3-b2cb-d70ff6eac7bc.png</url>
      <title>DEV Community: Spicy</title>
      <link>https://dev.to/spicykim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spicykim"/>
    <language>en</language>
    <item>
      <title>Adding passkeys to your app is way less code than you think</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:11:26 +0000</pubDate>
      <link>https://dev.to/spicykim/adding-passkeys-to-your-app-is-way-less-code-than-you-think-3f2k</link>
      <guid>https://dev.to/spicykim/adding-passkeys-to-your-app-is-way-less-code-than-you-think-3f2k</guid>
      <description>&lt;p&gt;Passwords are the problem, not the user. Over 80% of hacking-related breaches still involve stolen or weak credentials, and that number refuses to go down because the whole model is broken: breaches leak them, phishing steals them, people reuse them. (I wrote up the why, for non-developers, separately. Linked at the bottom.)&lt;/p&gt;

&lt;p&gt;Passkeys fix this at the root. There's no shared secret to steal, and they literally can't be phished because the credential is bound to your domain. The part most devs don't realize: adding them is surprisingly little code, especially if you lean on a library instead of wrestling raw WebAuthn ArrayBuffers.&lt;/p&gt;

&lt;p&gt;Here's a minimal flow using &lt;a href="https://simplewebauthn.dev/" rel="noopener noreferrer"&gt;SimpleWebAuthn&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration (creating a passkey)
&lt;/h2&gt;

&lt;p&gt;Server side, you hand the browser a challenge:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;generateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;verifyRegistrationResponse&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;@simplewebauthn/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="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="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="s1"&gt;My App&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveChallenge&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="nx"&gt;res&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;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="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="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedChallenge&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;getChallenge&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;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&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="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;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;await&lt;/span&gt; &lt;span class="nf"&gt;savePasskey&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;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="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&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;verified&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;verified&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;Browser side is almost nothing:&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="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="s1"&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerPasskey&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/register/options&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="s1"&gt;POST&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="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="p"&gt;)&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;attResp&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="na"&gt;optionsJSON&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="kd"&gt;const&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="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="s1"&gt;/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="s1"&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;headers&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;Content-Type&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;application/json&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;attResp&lt;/span&gt;&lt;span class="p"&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="p"&gt;)&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="k"&gt;if &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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Passkey registered&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;p&gt;That &lt;code&gt;startRegistration&lt;/code&gt; call is what pops the Face ID / fingerprint / PIN prompt. The browser and OS handle the scary crypto. You never touch a private key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login (using the passkey)
&lt;/h2&gt;

&lt;p&gt;Same shape, mirror image:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startAuthentication&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="s1"&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loginWithPasskey&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login/options&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="s1"&gt;POST&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="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="p"&gt;)&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;asseResp&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="na"&gt;optionsJSON&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="kd"&gt;const&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="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="s1"&gt;/login/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="s1"&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;headers&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;Content-Type&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;application/json&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;asseResp&lt;/span&gt;&lt;span class="p"&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="p"&gt;)&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="k"&gt;if &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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&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;p&gt;On the server, &lt;code&gt;generateAuthenticationOptions&lt;/code&gt; and &lt;code&gt;verifyAuthenticationResponse&lt;/code&gt; do the matching counterparts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things that trip people up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rpID must match your domain.&lt;/strong&gt; It's the registrable domain (&lt;code&gt;example.com&lt;/code&gt;), not a URL, no &lt;code&gt;https://&lt;/code&gt;, no path. On localhost, use &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;expectedOrigin must be exact,&lt;/strong&gt; including the scheme and port. A mismatch here is the number one "it works locally but not in prod" bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The challenge is single-use.&lt;/strong&gt; Generate it server-side, store it, verify against it once, throw it away. Don't let the client send its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's genuinely most of it. You can ship passkeys as an &lt;em&gt;option&lt;/em&gt; alongside passwords first, let users opt in, and watch your account-takeover numbers drop. No rip-and-replace required.&lt;/p&gt;

&lt;p&gt;If you want the non-technical version to send to your less-nerdy coworkers (or your boss who keeps asking "are passwords really that bad"), I wrote that up here: &lt;a href="https://lucas8.com/passkeys-vs-passwords-security/" rel="noopener noreferrer"&gt;The Password You've Been Using Just Got You Hacked&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyone shipped passkeys to real users yet? Curious how the opt-in rates have looked.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Give Claude a read-only straw to your SQLite DB: a tiny MCP server in Python</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Sun, 25 Oct 2026 05:43:19 +0000</pubDate>
      <link>https://dev.to/spicykim/give-claude-a-read-only-straw-to-your-sqlite-db-a-tiny-mcp-server-in-python-42fg</link>
      <guid>https://dev.to/spicykim/give-claude-a-read-only-straw-to-your-sqlite-db-a-tiny-mcp-server-in-python-42fg</guid>
      <description>&lt;p&gt;RAG was always a bit of a hack. Chunk your files, embed them, cross your fingers the right snippet comes back. Fine for fuzzy "what did this doc say" questions. Terrible for structured data, where you don't want a summary, you want the exact number.&lt;/p&gt;

&lt;p&gt;If your data lives in a database, there's a cleaner move in 2026: skip the vectors and let the model query the database directly through MCP (Model Context Protocol). The model writes SQL, your local server runs it, you get the real answer back. Nothing gets uploaded to anyone's cloud.&lt;/p&gt;

&lt;p&gt;Here's a minimal, working MCP server that does exactly that for SQLite. About 30 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server
&lt;/h2&gt;

&lt;p&gt;​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# server.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local-sqlite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DB_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_tables&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List the tables in the database.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB_PATH&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="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT name FROM sqlite_master WHERE type=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&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;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run a read-only SQL query and return the rows.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only SELECT queries are allowed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB_PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Row&lt;/span&gt;
    &lt;span class="k"&gt;try&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="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;&lt;span class="sb"&gt;``&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="n"&gt;Install&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;dependency&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s it:

​
{% raw %}
```bash
pip install &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp[cli]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
​```



## Wiring it into Claude Desktop

Drop this into your `claude_desktop_config.json`:

​

```json
{
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcpServers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {
    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local-sqlite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {
      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
      &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/absolute/path/to/server.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]
    }
  }
}
​```



Restart Claude Desktop and you can now ask &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;which five customers spent the most last month?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; The model calls `list_tables`, works out the schema, writes the SQL, calls `run_query`, and answers with real rows. Not a guess pulled from an embedded chunk. The actual data.

## The one line that matters most

This one:

​

```python
if not sql.lstrip().lower().startswith(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;):
    raise ValueError(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only SELECT queries are allowed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
​```



You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt; &lt;span class="n"&gt;handing&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;ability&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;SQL&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Without&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;creative&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="n"&gt;injection&lt;/span&gt; &lt;span class="n"&gt;hidden&lt;/span&gt; &lt;span class="n"&gt;inside&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;own&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;could&lt;/span&gt; &lt;span class="n"&gt;talk&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`DROP TABLE`&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt; &lt;span class="n"&gt;Keeping&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;cheapest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt; &lt;span class="n"&gt;rail&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="n"&gt;writes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;put&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt; &lt;span class="n"&gt;behind&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;separate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;explicit&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;its&lt;/span&gt; &lt;span class="n"&gt;own&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Never&lt;/span&gt; &lt;span class="n"&gt;through&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;few&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d add before trusting this with anything real:

- Open the connection read-only at the driver level too, not just by string-checking the query.
- Enforce a row limit so a `SELECT *` on a huge table doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;flood&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Log&lt;/span&gt; &lt;span class="n"&gt;every&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="n"&gt;see&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;actually&lt;/span&gt; &lt;span class="n"&gt;did&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;That&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the whole idea. The model gets a straw to the data, not a copy of it, and your database never leaves the machine.

I wrote up the bigger picture, the zero-trust angle and why this beats RAG for structured data, here: [The Local Data Architect: Connecting Your Private Database to AI via MCP](https://lucas8.com/mcp-local-data-architect-guide-postgresql-sqlite/)

Anyone else ripping out a RAG pipeline for direct MCP access? Curious what guardrails you&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt; &lt;span class="n"&gt;putting&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Windsurf vs Cursor vs Cline: how I'd actually pick one in 2026</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:51:19 +0000</pubDate>
      <link>https://dev.to/spicykim/windsurf-vs-cursor-vs-cline-how-id-actually-pick-one-in-2026-4ekg</link>
      <guid>https://dev.to/spicykim/windsurf-vs-cursor-vs-cline-how-id-actually-pick-one-in-2026-4ekg</guid>
      <description>&lt;p&gt;The autocomplete era is over. Nobody serious is still writing functions off ghost-text suggestions. The real question in 2026 isn't "should I use an AI coding tool," it's "which agent do I hand the keys to." And honestly, Windsurf, Cursor, and Cline are built for three genuinely different kinds of developer.&lt;/p&gt;

&lt;p&gt;Here's how I'd choose, minus the marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick Cursor if you want "it just works"
&lt;/h2&gt;

&lt;p&gt;Cursor is the safe default, and I mean that as a compliment. It's a VS Code fork, so there's no new environment to learn, your extensions and keybinds come along, and the whole thing is fast and polished.&lt;/p&gt;

&lt;p&gt;Its real strength is the multi-file refactor (Composer). Point it at a change that spans a dozen files and it handles the plumbing without going rogue. It's "half-agentic" on purpose: it does a lot, but still asks before it touches your terminal. For teams nervous about letting an AI run commands unsupervised, that restraint is the feature.&lt;/p&gt;

&lt;p&gt;Choose Cursor when you want capability without having to think about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick Windsurf if you want flow
&lt;/h2&gt;

&lt;p&gt;Windsurf feels different the moment you use it. Instead of a chat panel bolted onto an editor, the editor itself seems to understand your whole stack. You give it a goal and it chains the file edits and terminal steps together in a way that feels like delegation, not prompting.&lt;/p&gt;

&lt;p&gt;The tradeoff is trust. Windsurf wins when you're comfortable giving an agent more rope on multi-step tasks. If you like describing the outcome and letting the tool find the path, this is the one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick Cline if you want control and cheap tokens
&lt;/h2&gt;

&lt;p&gt;Cline is for people who don't want to be locked into a $20/month seat. It's an open-source extension with bring-your-own-key, so you plug in your own Claude (or whatever) API and pay for exactly what you use. Heavy users often come out cheaper, and you see every action it takes.&lt;/p&gt;

&lt;p&gt;The catch is it's more hands-on. You're trading polish for transparency and control. If that sounds like a feature instead of a chore, Cline is your tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual decision
&lt;/h2&gt;

&lt;p&gt;Strip the hype and it's almost this simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want safe and familiar?&lt;/strong&gt; Cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to delegate whole tasks?&lt;/strong&gt; Windsurf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want control and your own API bill?&lt;/strong&gt; Cline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no universal winner, which is the honest and slightly annoying truth. The best one is whichever matches how much autonomy you're actually comfortable handing over.&lt;/p&gt;

&lt;p&gt;I went deeper on each tool's context engine, pricing, and where they break down in the full comparison here: &lt;a href="https://lucas8.com/best-ai-coding-agents-windsurf-cursor-cline/" rel="noopener noreferrer"&gt;Best AI Coding Agents 2026: Windsurf vs Cursor vs Cline&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What are you running day to day? Curious whether people are sticking with Cursor or jumping to the fully agentic tools.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why HIPAA Doesn't Cover the Health Data Your API Just Pulled</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Sat, 04 Jul 2026 12:10:45 +0000</pubDate>
      <link>https://dev.to/spicykim/why-hipaa-doesnt-cover-the-health-data-your-api-just-pulled-3bjk</link>
      <guid>https://dev.to/spicykim/why-hipaa-doesnt-cover-the-health-data-your-api-just-pulled-3bjk</guid>
      <description>&lt;p&gt;If you're building anything on top of HealthKit, Google Fit, or a direct wearable API integration, there's a compliance assumption worth checking before you ship: HIPAA almost certainly doesn't apply to the data you're pulling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Covered Entity Problem
&lt;/h2&gt;

&lt;p&gt;HIPAA only regulates "covered entities" — hospitals, doctors, health insurers, and their direct business associates. A consumer wearable API (Apple HealthKit, Google Fit, Fitbit Web API, Oura API, Whoop API) doesn't make your app a covered entity just because it's handling heart rate or sleep data. Unless you're building specifically for a healthcare provider under a signed business associate agreement, you're outside HIPAA's scope entirely, according to a &lt;a href="https://www.halllawgroup.com/love-your-wearable-technology-know-what-youre-sharing/" rel="noopener noreferrer"&gt;legal breakdown from the Law Office of Jeffrey Hall&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That means the usual HIPAA checklist — encryption at rest, audit logs, minimum necessary access — is good practice, but it's not a legal requirement for most consumer-facing health apps. Nobody is going to fine you under HIPAA for over-collecting sleep data. That's the trap, not the reassurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Regulates This Data
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Regulation&lt;/th&gt;
&lt;th&gt;Applies to consumer wearable apps?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HIPAA&lt;/td&gt;
&lt;td&gt;No (unless working with a covered entity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR (EU users)&lt;/td&gt;
&lt;td&gt;Yes, as sensitive personal data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CCPA (California users)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Washington My Health My Data Act&lt;/td&gt;
&lt;td&gt;Yes, if any WA users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App store health data policies&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most interesting one for engineers is Washington's My Health My Data Act (RCW 19.373), passed in 2023. It's the first US state law written specifically to close the HIPAA gap for consumer health data, and it applies to any business with Washington users, not just Washington-based companies. If your app has any US user base, you likely have Washington users. The Act requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A separate, prominently linked Consumer Health Data Privacy Policy (not folded into your general privacy policy)&lt;/li&gt;
&lt;li&gt;Explicit opt-in consent before collection or sharing, not just a terms-of-service checkbox&lt;/li&gt;
&lt;li&gt;A functioning deletion request path across all systems, including backups&lt;/li&gt;
&lt;li&gt;Written authorization retained for six years if you ever sell health data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full text is at &lt;a href="https://www.atg.wa.gov/protecting-washingtonians-personal-health-data-and-privacy" rel="noopener noreferrer"&gt;RCW 19.373&lt;/a&gt; if you're scoping compliance work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Implementation Notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data retention.&lt;/strong&gt; If your backend keeps synced health metrics after a user deletes their account, you're not just accumulating liability, you're likely violating deletion-right requirements in at least one jurisdiction your users touch. Build a real deletion pipeline that hits backups, not just the primary table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party SDKs.&lt;/strong&gt; If you're piping HealthKit data into an analytics SDK, an ad network, or a crash reporter, check that SDK's data handling terms specifically. "Anonymized" health data has been shown to be re-identifiable at high accuracy when cross-referenced with other datasets, which is a real risk if you're forwarding granular biometric streams anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consent UX.&lt;/strong&gt; A single "Allow Health Access" iOS permission prompt is not equivalent to the affirmative, specific consent required under Washington's law or GDPR. If you're monetizing health data in any way (even aggregated), build a separate consent flow for that specific use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Employer/insurer integrations.&lt;/strong&gt; If you're building a wellness-program integration that feeds data to an employer or insurer, treat that data flow as high-risk by default. There's no HIPAA wall preventing that data from eventually informing risk decisions, and the legal exposure sits with whoever built the pipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;Most developer documentation for these APIs focuses entirely on the technical integration and barely mentions the regulatory landscape. Apple's HealthKit guidelines are the exception — they're genuinely strict about app review for health data access. Google Fit and most third-party wearable APIs leave compliance almost entirely up to you.&lt;/p&gt;

&lt;p&gt;If I were architecting a new wearable-data feature today, I'd default to treating every user as if they were covered by Washington's My Health My Data Act, since it's currently the strictest applicable standard and building to it covers most other jurisdictions by default.&lt;/p&gt;

&lt;p&gt;Full write-up with the consumer-facing angle and more on the legal background: &lt;a href="https://lucas8.com/wearable-health-data-hipaa-gap" rel="noopener noreferrer"&gt;https://lucas8.com/wearable-health-data-hipaa-gap&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>The Solar Tax Credit Died in 2026. Here's the Decision Tree.</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Sat, 04 Jul 2026 12:09:41 +0000</pubDate>
      <link>https://dev.to/spicykim/the-solar-tax-credit-died-in-2026-heres-the-decision-tree-3pd9</link>
      <guid>https://dev.to/spicykim/the-solar-tax-credit-died-in-2026-heres-the-decision-tree-3pd9</guid>
      <description>&lt;p&gt;If you priced out solar before 2026 and priced it out again this year, the numbers probably look worse. That's not a sales tactic or a market shift. The 30% federal residential solar tax credit (Section 25D) ended on December 31, 2025, with zero phase-down period. No grandfathering, no partial credit for contracts signed in 2025 but installed in 2026.&lt;/p&gt;

&lt;p&gt;That said, the credit's disappearance created a fork in the decision tree rather than a dead end. Here's the breakdown, without the sales pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Ended
&lt;/h2&gt;

&lt;p&gt;Section 25D let homeowners deduct 30% of system cost directly from federal taxes, with no cap and no income limit. It was legislated to run through 2034. The One Big Beautiful Bill Act, signed July 2025, moved the termination date to the end of 2025 instead — a nearly decade-early sunset. If your system was placed in service on or before December 31, 2025, you still claim the credit on that year's return. If it goes live in 2026 or later, the credit doesn't apply, full stop.&lt;/p&gt;

&lt;p&gt;The mechanism that survived is ownership structure. Section 25D covered direct ownership only. Section 48E, the commercial/business investment credit, is still active for entities that own solar equipment and lease or sell power from it. That single distinction is why "no tax credit" isn't accurate for every path to solar in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Remaining Paths
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Third-party ownership (lease/PPA).&lt;/strong&gt; A company owns the panels on your roof; you buy the electricity. That company can still claim the Section 48E credit and typically passes savings through as a lower monthly rate instead of a lump-sum deduction on your return. You lose equity-building and any ownership-dependent state incentives, but if your tax liability was too small to use 30% anyway, this closes most of the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. State-level programs, independent of federal policy.&lt;/strong&gt; These never depended on 25D and mostly survived untouched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State income tax credits (a minority of states)&lt;/li&gt;
&lt;li&gt;Sales tax exemptions on solar equipment (CA, FL, AZ, CO, NY, and others)&lt;/li&gt;
&lt;li&gt;Property tax exemptions so added home value doesn't raise your tax bill (30+ states)&lt;/li&gt;
&lt;li&gt;Production-based payments like New Jersey's SuSI program, which pays per kWh for 15 years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check your specific state through the &lt;a href="https://dsireusa.org/" rel="noopener noreferrer"&gt;Database of State Incentives for Renewables &amp;amp; Efficiency&lt;/a&gt;, the DOE-funded clearinghouse — coverage is wildly inconsistent state to state, so a generic "solar incentives still exist" claim is close to useless without checking your own address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Net metering and community solar.&lt;/strong&gt; Net metering — getting credited for excess power sent back to the grid — is a utility mechanism, not a tax credit, so it wasn't touched by the OBBBA at all. Community solar (buying a share of an off-site array instead of installing your own panels) extends this to renters and shaded-roof homes with zero upfront installation cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Who claims the credit&lt;/th&gt;
&lt;th&gt;You own the system&lt;/th&gt;
&lt;th&gt;Works if you have low tax liability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cash/loan purchase&lt;/td&gt;
&lt;td&gt;No one (credit gone)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No benefit either way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lease / PPA&lt;/td&gt;
&lt;td&gt;Third-party owner&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes — savings via lower rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State credit + exemptions&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Depends on state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net metering / community solar&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Yes — utility-side, not tax-side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;Most coverage treats this as one national story: "credit's gone, solar's still worth it long-term." That framing skips the part that actually matters — the size of the gap left behind depends entirely on your state's net metering rules and exemption stack, not on a national average payback period.&lt;/p&gt;

&lt;p&gt;If I were running these numbers for myself, I'd ask an installer for two quotes side by side: total cost with zero incentives applied, and total cost after every program that applies specifically to my address. If an installer can't produce both numbers without a follow-up call, they're probably still quoting off a pre-2026 script.&lt;/p&gt;

&lt;p&gt;The federal credit for direct ownership isn't coming back this cycle. But state programs, net metering, and lease structures are still on the table for most of the country, and stacking even two of them gets you closer to 2025's numbers than most headlines suggest.&lt;/p&gt;

&lt;p&gt;Full breakdown with a state-by-state incentive walkthrough: &lt;a href="https://lucas8.com/solar-tax-credit-alternatives-2026" rel="noopener noreferrer"&gt;https://lucas8.com/solar-tax-credit-alternatives-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Nutrition Label That Software Always Lacked</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:45:31 +0000</pubDate>
      <link>https://dev.to/spicykim/the-nutrition-label-that-software-always-lacked-213</link>
      <guid>https://dev.to/spicykim/the-nutrition-label-that-software-always-lacked-213</guid>
      <description>&lt;p&gt;On December 9, 2021, a security researcher posted a proof-of-concept exploit for a vulnerability in a Java library called Log4j. Within 72 hours, hundreds of millions of systems were at risk. The chaos that followed wasn't primarily about the vulnerability's severity. It was about something more fundamental: most organizations had no way to quickly search their own software to find out whether Log4j was buried somewhere inside it.&lt;/p&gt;

&lt;p&gt;That moment made the software bill of materials — SBOM — go from a niche compliance topic to a board-level conversation almost overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an SBOM?
&lt;/h2&gt;

&lt;p&gt;A software bill of materials is a structured, machine-readable inventory of every component inside a piece of software. Think of it as the nutrition label on a food package, applied to code. It tells you which open-source libraries, third-party packages, and internal components make up a given application — along with their versions, licenses, and any known vulnerabilities.&lt;/p&gt;

&lt;p&gt;Modern software is rarely written entirely from scratch. A typical commercial application might contain 500 to 1,500 third-party dependencies by the time all transitive relationships are counted. Without an SBOM, tracking all of those relationships manually is effectively impossible.&lt;/p&gt;

&lt;p&gt;Two open standards dominate today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPDX (Software Package Data Exchange)
  Origin: Linux Foundation
  Primary focus: License compliance
  Best for: Teams where legal is the main concern

CycloneDX
  Origin: OWASP
  Primary focus: Security and vulnerability tracking
  Best for: DevSecOps and security-focused teams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Log4Shell Made This Non-Negotiable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2021-44228" rel="noopener noreferrer"&gt;The Log4Shell vulnerability — CVE-2021-44228&lt;/a&gt; affected an estimated 3 billion devices. But the more instructive part of the story was the response time gap.&lt;/p&gt;

&lt;p&gt;Companies that had SBOMs — even rough, incomplete ones — could run an automated search and know within hours whether they were affected. Companies without them spent weeks manually auditing every application, tracing every dependency tree, and waiting on vendor responses.&lt;/p&gt;

&lt;p&gt;The 2024 XZ Utils backdoor reinforced the same point. An attacker spent nearly two years contributing to an open-source project before inserting a hidden backdoor into a widely used compression library. Most organizations still had no automated way to know whether that library was in their stack. Supply chain attacks exploit exactly the visibility gap that an SBOM closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Requires SBOM Now
&lt;/h2&gt;

&lt;p&gt;Compliance pressure has moved faster than most timelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;US Executive Order 14028 (May 2021)
  → Federal software vendors must provide SBOMs
  → Status: Active

FDA Medical Device Guidance (Oct 2023)
  → SBOM required with every premarket submission
  → Status: Active

EU Cyber Resilience Act (Dec 2027)
  → Covers all software and connected devices sold in the EU
  → SBOM documentation explicitly required
  → Status: Full enforcement December 2027
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to ENISA's 2026 figures, 78% of enterprises have begun SBOM adoption. A DigiCert survey found that 43% of companies expect to face explicit SBOM requirements within 24 months. The gap between "starting adoption" and "production-ready compliance" remains substantial for most of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Get Started
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.cisa.gov/sbom" rel="noopener noreferrer"&gt;CISA maintains a comprehensive SBOM resource library&lt;/a&gt; covering formats, tooling, and implementation guidance. For most teams, the practical starting point looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Pick a standard
  → Security-first team: CycloneDX
  → License compliance priority: SPDX

Step 2: Integrate automated generation
  → Open source: Syft (Anchore) — generates from container images and filesystems
  → Open source: Grype — vulnerability scanning against generated SBOMs
  → Commercial: FOSSA, Snyk, Black Duck — CI/CD pipeline integration

Step 3: Connect to a vulnerability database
  → NIST NVD
  → OSV (Open Source Vulnerabilities)
  → Commercial feeds from vendors above

Step 4: Establish update cadence
  → Tie SBOM generation to every build, not a quarterly audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with a single application. Attempting to retrofit SBOM generation across an entire portfolio at once is where most programs stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;The framing that dominates most SBOM coverage — "generate an SBOM, know what's in your software, fix vulnerabilities faster" — is accurate but incomplete in a way that matters practically.&lt;/p&gt;

&lt;p&gt;Most organizations that invest in SBOM tooling discover the same thing in the first few months: the SBOM reveals a dependency tree that's far messier than anyone expected, and the organizational process for responding to what it reveals doesn't exist yet.&lt;/p&gt;

&lt;p&gt;An SBOM is a diagnostic tool. It tells you what's there. But knowing your application has 847 dependencies, 23 of which have known CVEs, doesn't automatically generate a prioritization framework, a patch ownership model, or a vendor communication process.&lt;/p&gt;

&lt;p&gt;The companies that responded fastest to Log4Shell weren't the ones with the most sophisticated tooling. They were the ones that had already answered "who owns this dependency when something goes wrong?" before the crisis hit. The SBOM tells you what you have. The harder question it surfaces is whether your organization is structured to act on it.&lt;/p&gt;




&lt;p&gt;Full piece with more detail and external sources: &lt;a href="https://lucas8.com/what-is-sbom-software-bill-of-materials/" rel="noopener noreferrer"&gt;lucas8.com/what-is-sbom-software-bill-of-materials&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Is AI to Blame for Your Rising Electric Bill?</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:06:30 +0000</pubDate>
      <link>https://dev.to/spicykim/is-ai-to-blame-for-your-rising-electric-bill-1kd</link>
      <guid>https://dev.to/spicykim/is-ai-to-blame-for-your-rising-electric-bill-1kd</guid>
      <description>&lt;p&gt;In January 2026, a Virginia man received an electric bill for $281 — nearly triple the $100 he had paid the previous month. He had lived in the same house for 40 years. Nothing in his home had changed. What had changed was what surrounded it: Northern Virginia's "Data Center Alley," the largest concentration of AI data centers on the planet.&lt;/p&gt;

&lt;p&gt;The question he started asking has become one of the defining consumer issues of 2026: are AI data centers genuinely responsible for rising electric bills? The honest answer is more nuanced than most headlines suggest. But the short version is: yes, partly, and the share is growing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Electricity Do AI Data Centers Actually Use?
&lt;/h2&gt;

&lt;p&gt;The numbers are genuinely staggering. According to the IEA, data centers accounted for roughly half of all new electricity demand growth in the US last year — a share the agency expects to hold through 2030.&lt;/p&gt;

&lt;p&gt;Total US data center energy demand is projected to nearly double between 2025 and 2028, jumping from 80 to 150 gigawatts. That 70-gigawatt addition is roughly equivalent to the entire annual electricity consumption of Spain added to the US grid in just three years.&lt;/p&gt;

&lt;p&gt;Virginia alone now hosts nearly 600 data centers, with facilities accounting for close to 40 percent of all electricity used in the state in 2024. The race to find enough power for this demand has pushed the energy industry toward nuclear and other alternative energy sources, turning what was once a tech industry concern into a kitchen-table issue for millions of American households.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Connection to Your Bill Is Real
&lt;/h2&gt;

&lt;p&gt;The mechanism isn't a direct surcharge. But the path from data center construction to higher household costs is real, and it runs through the utility infrastructure system.&lt;/p&gt;

&lt;p&gt;When a data center connects to the grid, utilities must upgrade infrastructure to handle the added load — new transmission lines, transformers, generation capacity. Those investments get approved by state regulators and recovered through rate increases spread across all customers in the service territory, including residential ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cnbc.com/2026/02/12/electricity-price-data-center-ai-inflation-goldman.html" rel="noopener noreferrer"&gt;Goldman Sachs reported&lt;/a&gt; that US electricity prices jumped 6.9% in 2025 — more than double the headline inflation rate — and forecasts an additional 6% increase through 2027. In areas with heavy data center concentrations, Bloomberg found electricity costs rose 267% over the past five years.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://advocacy.consumerreports.org/press_release/consumer-reports-new-survey-finds-americans-are-skeptical-of-big-techs-pledge-to-pay-for-all-ai-data-center-energy-costs" rel="noopener noreferrer"&gt;Consumer Reports survey from May 2026&lt;/a&gt; found that 78% of Americans are concerned that data centers will continue raising their household energy bills. And when told about the "Ratepayer Protection Pledge" — a White House-sponsored agreement signed by Amazon, Google, Meta, Microsoft, OpenAI, Oracle, and xAI pledging to cover their full energy costs — 75% said they were not confident companies would follow through.&lt;/p&gt;

&lt;h2&gt;
  
  
  But Is AI the Only Culprit?
&lt;/h2&gt;

&lt;p&gt;Here's where the honest answer gets complicated.&lt;/p&gt;

&lt;p&gt;US residential electricity prices rose more than 30% between 2021 and early 2026 — a trend that began well before ChatGPT launched. Aging grid infrastructure, climate change, coal and natural gas plant closures, and structural issues in regional electricity markets were already pushing bills upward before hyperscalers started adding hundreds of gigawatts of demand.&lt;/p&gt;

&lt;p&gt;SemiAnalysis published a detailed analysis arguing that an obscure capacity auction mechanism in the PJM market — covering 13 mid-Atlantic and Midwest states — accounts for much of the electricity price surge, with data center load playing an amplifying but not singular role.&lt;/p&gt;

&lt;p&gt;The most accurate framing: AI data centers are a real and growing contributor to higher bills, but they're landing on top of a system that was already struggling with affordability. Your bills would be lower without the data center boom. They wouldn't be cheap regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;What surprised me wasn't the scale of the numbers — those are well documented. It was how clearly the politics had already shifted by mid-2026.&lt;/p&gt;

&lt;p&gt;In 2023 and 2024, criticism of data center energy use came mostly from environmental advocates and local communities. By 2026, it had become a mainstream voter concern: bipartisan calls in Congress, gubernatorial campaigns in Virginia and New Jersey fought partly on utility affordability, a Maine moratorium on new data center construction, and a White House pledge that would not have existed if this weren't a live political issue at the presidential level.&lt;/p&gt;

&lt;p&gt;The practical implication for a household is limited — there's no individual action that fully insulates you from regional rate increases driven by infrastructure decisions made at the utility and regulatory level. The most effective lever is engagement with state public utility commission proceedings, where rate increase requests are actually approved or rejected.&lt;/p&gt;

&lt;p&gt;The Virginia man with the $281 bill was right to ask the question. The full answer is complicated, but it starts with the same honest admission: the data centers that run the AI tools we use every day are not free, and someone is paying for them.&lt;/p&gt;




&lt;p&gt;Full piece with more detail and sources: &lt;a href="https://lucas8.com/ai-data-center-electric-bill/" rel="noopener noreferrer"&gt;lucas8.com/ai-data-center-electric-bill&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>That QR Code on the Parking Meter Might Be a Scam</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:35:19 +0000</pubDate>
      <link>https://dev.to/spicykim/that-qr-code-on-the-parking-meter-might-be-a-scam-54hp</link>
      <guid>https://dev.to/spicykim/that-qr-code-on-the-parking-meter-might-be-a-scam-54hp</guid>
      <description>&lt;p&gt;You scan QR codes constantly without thinking about it — the restaurant menu, the parking meter, the flyer taped to a lamppost. A QR code scam usually doesn't look like a scam at all, which is exactly the problem. It looks like a sticker. It looks like part of the wallpaper.&lt;/p&gt;

&lt;p&gt;That gap between how QR codes look and what they can actually do is where quishing — QR code phishing — has quietly become one of the fastest-growing scam categories of 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  How QR Code Scams Work
&lt;/h2&gt;

&lt;p&gt;A QR code scam works because the malicious part is invisible until the moment you scan it. Unlike a phishing email, where a suspicious link sits in plain text, a QR code hides its destination inside a pattern of black and white squares that no human can read without a camera.&lt;/p&gt;

&lt;p&gt;Attackers exploit that blind spot by printing their own code on a sticker and placing it directly over a legitimate one — on a parking meter, a restaurant table tent, a flyer, or a delivery package. The fake code usually leads to a cloned login page, a fraudulent payment screen, or a malware download.&lt;/p&gt;

&lt;p&gt;This isn't a small trend. Quishing incidents jumped 146% in the first quarter of 2026 alone, with nearly 18.7 million cases recorded in March, according to threat intelligence data reported by &lt;a href="https://www.wataugademocrat.com/ap/state/quishing-surges-146-in-q1-2026-as-attackers-hide-behind-the-code/article_121840ac-c8d4-5e08-8c97-7cadd00ed686.html" rel="noopener noreferrer"&gt;Watauga Democrat&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shows Up Most
&lt;/h2&gt;

&lt;p&gt;Public parking meters are one of the most common targets, since the sticker format is easy to replicate. Police departments in Denver and Austin have documented fake QR stickers placed over legitimate parking codes, redirecting drivers to payment pages that steal card details.&lt;/p&gt;

&lt;p&gt;Restaurant table tents, parcel delivery notices, fake court summons, and event posters round out the most common categories — all relying on the same trick: a context where scanning feels expected, not suspicious.&lt;/p&gt;

&lt;p&gt;Email-based quishing has also grown. QR codes embedded in PDF attachments or images slip past traditional phishing filters that only scan visible text. According to the &lt;a href="https://www.isthisqrsafe.com/learn/quishing-complete-guide" rel="noopener noreferrer"&gt;complete quishing guide from Is This QR Safe&lt;/a&gt;, this is exactly why security teams describe it as a blind spot in standard email defenses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Even Careful People Fall For It
&lt;/h2&gt;

&lt;p&gt;Most people scan QR codes without checking the destination first, and surveys have found that the majority of consumers can't reliably tell a malicious code from a legitimate one just by looking at it. High trust plus low verification is exactly what gives quishing room to grow even as awareness of regular email phishing improves.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Spot a Fake Code Before You Scan
&lt;/h2&gt;

&lt;p&gt;Look at the sticker itself before you look at your phone. A code that's crooked, layered on top of a different sticker, or peeling at one corner is a strong sign someone placed it there after the fact.&lt;/p&gt;

&lt;p&gt;Use your phone's built-in camera preview instead of a dedicated scanning app whenever possible. Most modern phones show you the destination URL before opening it — check whether the domain matches who you'd expect.&lt;/p&gt;

&lt;p&gt;Dynamic QR codes route through a shortened link before reaching their final destination, which makes the preview less useful on its own. In those cases, look at whether the page that finally loads matches the branding, fonts, and layout you'd expect.&lt;/p&gt;

&lt;p&gt;Pause on anything that asks you to log in or enter payment details immediately after scanning. Legitimate parking apps and ordering systems rarely require fresh credentials every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;What surprised me digging into this wasn't how sophisticated these scams are — it's how little sophistication they need. Most of the fake QR codes documented by police departments weren't elaborate forgeries. They were printer-paper stickers, sometimes a visibly different shade of white than the surface underneath, placed by someone who knew nobody really inspects a parking meter before paying it.&lt;/p&gt;

&lt;p&gt;Most security advice focuses on apps and scanner tools that preview links before opening them. Those help, but they miss the simpler habit that actually prevents most of these scams: physically looking at the sticker for two seconds before you scan, not just checking the link after you've already decided to trust it.&lt;/p&gt;

&lt;p&gt;If I had to pick one habit to actually keep, it would be treating any QR code on a payment-related surface — meters, parking lots, toll booths — as default-suspicious until proven otherwise. Those are the highest-value, lowest-effort targets for this exact scam.&lt;/p&gt;




&lt;p&gt;Full piece with more detail and visuals: &lt;a href="https://lucas8.com/qr-code-scam-parking-meter/" rel="noopener noreferrer"&gt;lucas8.com/qr-code-scam-parking-meter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Samsung vs LG vs Roku vs Fire TV vs Apple TV: Which One Tracks You the Least?</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Sun, 28 Jun 2026 15:33:23 +0000</pubDate>
      <link>https://dev.to/spicykim/samsung-vs-lg-vs-roku-vs-fire-tv-vs-apple-tv-which-one-tracks-you-the-least-14em</link>
      <guid>https://dev.to/spicykim/samsung-vs-lg-vs-roku-vs-fire-tv-vs-apple-tv-which-one-tracks-you-the-least-14em</guid>
      <description>&lt;p&gt;Your smart TV is running a feature you almost certainly never turned on.&lt;/p&gt;

&lt;p&gt;It's called ACR — Automatic Content Recognition. It works like Shazam, but instead of identifying songs on request, it runs continuously in the background: capturing screenshots of whatever is on your screen, matching those frames against a content database, and sending that viewing history to advertisers and data brokers. It tracks everything — streaming apps, live TV, and devices plugged into HDMI like your game console or cable box.&lt;/p&gt;

&lt;p&gt;In December 2025, Texas Attorney General Ken Paxton sued Samsung, LG, Sony, Hisense, and TCL over ACR, calling it "an uninvited, invisible digital invader." His office obtained temporary restraining orders against Samsung and Hisense while the cases proceed.&lt;/p&gt;

&lt;p&gt;Here's how the six major platforms actually compare.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;ACR Used&lt;/th&gt;
&lt;th&gt;HDMI Tracking&lt;/th&gt;
&lt;th&gt;On by Default&lt;/th&gt;
&lt;th&gt;Data Sold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Samsung Tizen&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LG webOS&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roku&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google TV&lt;/td&gt;
&lt;td&gt;⚠️ Platform: No / Brand: Varies&lt;/td&gt;
&lt;td&gt;⚠️ Varies&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Fire TV&lt;/td&gt;
&lt;td&gt;❌ No (HDMI)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple TV 4K&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Samsung Tizen
&lt;/h2&gt;

&lt;p&gt;ACR is called "Viewing Information Services" — not ACR — which is why most users never find it. It's buried under &lt;strong&gt;Settings → Support → Terms &amp;amp; Policy&lt;/strong&gt;, not in the Privacy menu.&lt;/p&gt;

&lt;p&gt;Texas obtained a temporary restraining order against Samsung specifically. The court agreed there was sufficient reason to pause data collection while the lawsuit proceeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Support → Terms &amp;amp; Policy → Viewing Information Services → Off
Also disable: Interest-Based Advertisement Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  LG webOS
&lt;/h2&gt;

&lt;p&gt;LG's ACR is called "Live Plus." It defaults to on and tracks content across HDMI inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → All Settings → General → Live Plus → Off
Settings → All Settings → General → About This TV → User Agreements → Viewing Information → Off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate settings, not one. Missing the second one still leaves partial tracking enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roku
&lt;/h2&gt;

&lt;p&gt;Roku is the most transparent about ACR — its published documentation explicitly states that collected data is shared with third parties and that previously collected data is retained even if you disable the feature later.&lt;/p&gt;

&lt;p&gt;Disabling ACR on Roku stops HDMI-based tracking but doesn't affect data collected from Roku's own streaming channels. That requires separate steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Privacy → Smart TV Experience → Use Info from TV Inputs → uncheck
Settings → Privacy → Advertising → Limit Ad Tracking → on
Settings → Privacy → Privacy Choices → disable data sharing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Google TV (Sony, TCL, Philips, Hisense)
&lt;/h2&gt;

&lt;p&gt;Google's platform doesn't use ACR directly. But the individual TV brand (Sony, TCL, etc.) may add its own ACR layer separately. And Google's own data collection from the platform — searches, YouTube viewing, app usage — feeds into the same ad profile used across all your Google-connected devices. There's no opting out of Google's core policies if you want smart TV functionality.&lt;/p&gt;

&lt;p&gt;Check for brand-specific ACR under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Device Preferences → Samba Interactive TV → Disable
or
Settings → Display &amp;amp; Sound → Intelligent Settings → all off
Settings → Privacy → Usage &amp;amp; Diagnostics → off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Amazon Fire TV
&lt;/h2&gt;

&lt;p&gt;Amazon has publicly confirmed it doesn't use ACR to track content from HDMI-connected devices. That's the key differentiator from every other platform in this list.&lt;/p&gt;

&lt;p&gt;It still collects data on what you watch through antenna inputs and Fire TV streaming apps. But the absence of HDMI ACR tracking is a meaningful privacy win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tighten it further:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Preferences → Privacy Settings → Collect App and Over-the-Air Usage Data → off
Settings → Preferences → Privacy Settings → Interest-Based Ads → off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Apple TV 4K
&lt;/h2&gt;

&lt;p&gt;No ACR. Full stop. Apple has confirmed this, and independent research supports it.&lt;/p&gt;

&lt;p&gt;Apple does collect some usage data within its own apps, but processes it through differential privacy — anonymized in aggregate before use. Third-party apps must explicitly request tracking permission under tvOS, which you can block globally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Privacy → Tracking → Allow Apps to Request to Track → off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat: Apple TV is a streaming box, not a TV. If the physical TV it's plugged into has ACR enabled, that TV's OS is still screenshotting whatever Apple TV displays. The correct setup is Apple TV as your streaming device &lt;em&gt;plus&lt;/em&gt; ACR disabled on the TV itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Most Guides Miss
&lt;/h2&gt;

&lt;p&gt;Disabling ACR at the OS level doesn't stop individual streaming apps from tracking you — Netflix, YouTube, and every other app collect data separately under their own policies. You need to treat the TV's OS and each app as separate tracking systems.&lt;/p&gt;

&lt;p&gt;Also: firmware updates on Samsung, LG, and Roku have been documented to reset privacy settings to their defaults. Set a reminder to recheck after every major update.&lt;/p&gt;




&lt;p&gt;The practical hierarchy if privacy matters to you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apple TV 4K&lt;/strong&gt; — no ACR, anonymized data, explicit tracking consent model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Fire TV&lt;/strong&gt; — no HDMI ACR, still collects streaming data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google TV&lt;/strong&gt; — platform ACR-free, but brand ACR and Google's data model apply&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roku&lt;/strong&gt; — ACR on by default, unusually transparent documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LG webOS&lt;/strong&gt; — ACR on by default, requires two separate settings to disable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samsung Tizen&lt;/strong&gt; — ACR on by default, buried deepest in legal menus&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full breakdown with per-brand step-by-step settings: &lt;a href="https://lucas8.com/smart-tv-acr-privacy-comparison" rel="noopener noreferrer"&gt;https://lucas8.com/smart-tv-acr-privacy-comparison&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Baby Monitor's Biggest Security Flaw Isn't Hackers. It's the Company That Built It.</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:50:56 +0000</pubDate>
      <link>https://dev.to/spicykim/your-baby-monitors-biggest-security-flaw-isnt-hackers-its-the-company-that-built-it-5hbj</link>
      <guid>https://dev.to/spicykim/your-baby-monitors-biggest-security-flaw-isnt-hackers-its-the-company-that-built-it-5hbj</guid>
      <description>&lt;p&gt;In May 2026, a French ethical hacker named Sammy Azdoufal bought a baby monitor off Amazon and spent a few hours looking at its network traffic. What he found: 1.1 million cameras across 300+ brand names, all running on the same shared platform, accessible to anyone with a free account. No password cracking. No exploit chain. He clicked a URL and got the image.&lt;/p&gt;

&lt;p&gt;The vulnerability wasn't a clever attack. It was negligence — hardcoded credentials, an MQTT broker with no per-device access controls, and motion-alert images sitting on an Alibaba OSS bucket with no authentication required.&lt;/p&gt;

&lt;p&gt;This is the actual baby monitor security problem. Not a stranger breaking in through your Wi-Fi. The architecture itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The White-Label Problem
&lt;/h2&gt;

&lt;p&gt;Most budget smart cameras on Amazon are the same product under different names.&lt;/p&gt;

&lt;p&gt;Meari Technology (Hangzhou, China) supplies hardware, software, and cloud infrastructure to 300+ brands. When you buy a monitor you've never heard of — or even some you have — there's a real chance it shares a backend platform with hundreds of other products. The box doesn't disclose which cloud it connects to. The app is often interchangeable across brands.&lt;/p&gt;

&lt;p&gt;A flaw at the platform level means millions of devices are exposed simultaneously, regardless of brand name.&lt;/p&gt;

&lt;p&gt;Rapid7 tested nine popular baby monitors and gave eight of them an "F" for security. Higher prices didn't correlate with better security — more features meant more attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Encrypted" Actually Means Here
&lt;/h2&gt;

&lt;p&gt;Many monitors market end-to-end encryption. Most don't implement it correctly.&lt;/p&gt;

&lt;p&gt;Real E2E encryption: only your device and your app can decrypt the stream — not the manufacturer's servers.&lt;/p&gt;

&lt;p&gt;What most Wi-Fi monitors actually do: encrypt the leg from your camera to their servers, and the leg from their servers to your app. The manufacturer sees plaintext at the server level.&lt;/p&gt;

&lt;p&gt;The Meari MQTT broker (CVE-2026-33356) let any authenticated platform account subscribe to camera activity across all devices on a regional broker. Azdoufal observed 2,000+ device messages within minutes from a single broker endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Reduces Risk
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Network segmentation&lt;/strong&gt; — most underused, highest impact. Put your baby monitor on a guest Wi-Fi VLAN, isolated from your main devices. A compromised monitor can't pivot to your laptop or NAS. Takes 5 minutes on most home routers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firmware updates over passwords&lt;/strong&gt; — a strong password won't protect you if the backend has an unpatched CVE. Enable auto-updates or check monthly. This matters more than password complexity on a vulnerable platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brand selection criteria that actually signal security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Published vulnerability disclosure program or bug bounty&lt;/li&gt;
&lt;li&gt;Documented patch history (not just "we take security seriously")&lt;/li&gt;
&lt;li&gt;Privacy policy that specifies data collected during idle operation — not just when you're actively viewing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What doesn't move the needle as much:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong passwords alone on a platform-level vulnerability&lt;/li&gt;
&lt;li&gt;"No cloud" marketing that still phones home for firmware and analytics&lt;/li&gt;
&lt;li&gt;Price as a proxy for security&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Regulatory Gap
&lt;/h2&gt;

&lt;p&gt;The FCC's US Cyber Trust Mark — a voluntary IoT security labeling program — is still in development. "Good cybersecurity is invisible to consumers. They can't tell what products are risky," Stacey Higginbotham of Consumer Reports said following the Meari disclosure.&lt;/p&gt;

&lt;p&gt;Until mandatory baseline security requirements exist for IoT devices sold in the US, the research burden falls on buyers. Consumer Reports now runs security and privacy evaluations on baby monitors specifically — worth checking before purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Monitor is on a dedicated guest/IoT VLAN
[ ] Firmware auto-updates enabled (or manual check set monthly)
[ ] Default credentials changed immediately on setup
[ ] Brand has published vulnerability disclosure program
[ ] Privacy policy reviewed for third-party data sharing
[ ] App permissions audited (disable anything not needed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Meari disclosure made the problem concrete at a scale that's hard to ignore. 1.1 million cameras. Five critical CVEs. Footage of children's bedrooms accessible to anyone with a free account and a few minutes.&lt;/p&gt;

&lt;p&gt;That's not a hacking story. It's a product design story.&lt;/p&gt;




&lt;p&gt;Full breakdown: &lt;a href="https://lucas8.com/baby-monitor-privacy-risks" rel="noopener noreferrer"&gt;https://lucas8.com/baby-monitor-privacy-risks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Two-Factor Auth Isn't the Shield You Think It Is.</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:58:28 +0000</pubDate>
      <link>https://dev.to/spicykim/two-factor-auth-isnt-the-shield-you-think-it-is-5c77</link>
      <guid>https://dev.to/spicykim/two-factor-auth-isnt-the-shield-you-think-it-is-5c77</guid>
      <description>&lt;p&gt;You enabled two-factor authentication. Good call. But here's what most security guides skip: the biggest account takeovers of the past three years didn't require attackers to crack your 2FA code at all. They found ways to make you hand it over — or made the authentication step irrelevant entirely.&lt;/p&gt;

&lt;p&gt;MFA fatigue attacks are now one of the most documented techniques in real breaches, and they work precisely because 2FA gave everyone a false sense of being done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 2FA Still Gets Beaten in 2026
&lt;/h2&gt;

&lt;p&gt;Traditional 2FA was designed to stop credential stuffing — someone stealing your password and trying to log in. For that specific threat, it works brilliantly. Microsoft's data shows MFA blocks over 99% of automated credential attacks.&lt;/p&gt;

&lt;p&gt;The problem is the threat didn't stay still. Attackers noticed 2FA made the password less valuable, so they shifted focus: steal the session, exhaust the user, or reroute the auth flow entirely. These aren't exotic techniques. They show up in the Verizon DBIR every year, and the companies hit aren't careless ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Techniques Hackers Use to Walk Past MFA
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. MFA Fatigue (Push Bombing)
&lt;/h3&gt;

&lt;p&gt;An attacker who has your username and password triggers your push notification repeatedly — sometimes dozens of times overnight. Most people eventually tap "Approve" to make it stop. That single tap hands over a valid session token.&lt;/p&gt;

&lt;p&gt;This exact method took down Uber in 2022. The attacker bought leaked credentials, bombed the contractor's phone with push requests, then sent a WhatsApp message posing as Uber IT saying "approve once and it'll stop." The contractor did. Game over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Enable number matching on push apps. You type a two-digit code shown on your screen into the push prompt — blind approvals stop working immediately. Microsoft Authenticator and Duo both support this and Microsoft enabled it by default across Entra ID in 2023.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Adversary-in-the-Middle (AiTM) Attacks
&lt;/h3&gt;

&lt;p&gt;An attacker sets up a proxy that looks identical to a real login page. When you log in through it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The proxy relays your credentials to the real site&lt;/li&gt;
&lt;li&gt;The real site sends back a 2FA challenge — the proxy relays that too&lt;/li&gt;
&lt;li&gt;You enter your code — the proxy captures your authenticated &lt;strong&gt;session cookie&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The attacker replays that cookie from a clean browser, no further auth required
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You → [Fake Proxy] → Real Site
             ↓
      Session Cookie stolen
             ↓
Attacker → [Replays Cookie] → Logged In
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session cookie is the key. Your 2FA was used — legitimately — to create a session the attacker now owns. The Verizon 2025 DBIR flags stolen session tokens as a growing proportion of breach vectors for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Only passkeys and hardware security keys are immune to this. They use cryptographic proofs tied to the exact origin domain — a proxy gets a different challenge and the login fails silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SIM Swapping
&lt;/h3&gt;

&lt;p&gt;An attacker calls your carrier posing as you — armed with info from data brokers or social media — and convinces a rep to transfer your number to a SIM they control. From that point, every SMS code sent to your phone goes to them instead.&lt;/p&gt;

&lt;p&gt;No malware. Nothing on your device. The FTC has received thousands of SIM swap reports annually since 2021.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Move off SMS 2FA entirely for anything financial or high-value. Authenticator apps (Google Authenticator, Aegis, Authy) generate codes locally on your device — a SIM swap can't intercept them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2FA Method Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Stops Credential Stuffing&lt;/th&gt;
&lt;th&gt;Stops Push Bombing&lt;/th&gt;
&lt;th&gt;Stops AiTM&lt;/th&gt;
&lt;th&gt;Stops SIM Swap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMS OTP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authenticator App (TOTP)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push (standard)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push + Number Matching&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware Key / Passkey&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What "Phishing-Resistant" Actually Means
&lt;/h2&gt;

&lt;p&gt;Phishing-resistant MFA uses public-key cryptography where your device proves it's physically present at the &lt;strong&gt;real&lt;/strong&gt; domain. There's no code to intercept, no push to trick you into approving, no SMS to redirect.&lt;/p&gt;

&lt;p&gt;Both hardware security keys (YubiKey etc.) and passkeys qualify. FIDO Alliance reports 5 billion active passkeys worldwide as of 2026, and 68% of organizations surveyed are actively deploying them for employee sign-in. Google, Apple, and Microsoft have all made passkeys the default for new accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Upgrade Path
&lt;/h2&gt;

&lt;p&gt;You don't need to switch everything at once:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This week:&lt;/strong&gt; Remove SMS 2FA from any financial account. Switch to an authenticator app — takes ~30 seconds per account in settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This month:&lt;/strong&gt; Enable number matching on any push-based MFA app. Check your organization's Entra ID or Okta settings to confirm it's on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When ready:&lt;/strong&gt; Add a hardware key ($25–55 for a basic YubiKey) for email, password manager, and any admin access. Set up passkeys on Google, Apple, and GitHub — each takes under three minutes.&lt;/p&gt;




&lt;p&gt;The goal isn't to scare you off 2FA — it's to make sure what you're running matches the current threat, not the threat from 2019. Getting off SMS and onto an authenticator app this week already moves you out of the most exploited tier.&lt;/p&gt;

&lt;p&gt;Full breakdown with real breach examples: &lt;a href="https://lucas8.com/mfa-fatigue-attack-two-factor-bypass" rel="noopener noreferrer"&gt;https://lucas8.com/mfa-fatigue-attack-two-factor-bypass&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>Browser Fingerprinting in Practice — The Signals, the Math, and What Actually Defeats It</title>
      <dc:creator>Spicy</dc:creator>
      <pubDate>Thu, 18 Jun 2026 16:38:31 +0000</pubDate>
      <link>https://dev.to/spicykim/browser-fingerprinting-in-practice-the-signals-the-math-and-what-actually-defeats-it-15k0</link>
      <guid>https://dev.to/spicykim/browser-fingerprinting-in-practice-the-signals-the-math-and-what-actually-defeats-it-15k0</guid>
      <description>&lt;p&gt;Most privacy advice still centers on cookies — clear them, block them, use incognito. Meanwhile, fingerprinting has become the dominant tracking method precisely because it doesn't touch cookies at all. Here's what's actually happening at the API level, and what countermeasures hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Signals (And the Code Behind Them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canvas fingerprinting&lt;/strong&gt; exploits subtle rendering differences between GPU/driver combinations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCanvasFingerprint&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw text with specific font, size, and emoji — &lt;/span&gt;
  &lt;span class="c1"&gt;// rendering varies by OS font rasterizer and GPU&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14px Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;125&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="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#069&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cwm fjordbank glyphs vext quiz 🎮&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(102, 204, 0, 0.7)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cwm fjordbank glyphs vext quiz 🎮&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Hash the resulting pixel data&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Hash the output for compact comparison&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;hashFingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataUrl&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;encoder&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encoder&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;dataUrl&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;hashBuffer&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashBuffer&lt;/span&gt;&lt;span class="p"&gt;))&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;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&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;p&gt;The same code produces different pixel-level output across GPU vendors (NVIDIA vs AMD vs Apple Silicon), driver versions, and even font hinting settings — none of which the user can see, but all of which produce a consistent hash per device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebGL fingerprinting&lt;/strong&gt; goes deeper into hardware identification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWebGLFingerprint&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&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;gl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;experimental-webgl&lt;/span&gt;&lt;span class="dl"&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;gl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;debugInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEBGL_debug_renderer_info&lt;/span&gt;&lt;span class="dl"&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="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debugInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNMASKED_VENDOR_WEBGL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debugInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNMASKED_RENDERER_WEBGL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// e.g. "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11..."&lt;/span&gt;
    &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSupportedExtensions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;maxTextureSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAX_TEXTURE_SIZE&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 often reveals your exact GPU model, which on its own significantly narrows the population of matching devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AudioContext fingerprinting&lt;/strong&gt; uses hardware-dependent audio processing variance:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAudioFingerprint&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;audioCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new &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="nx"&gt;AudioContext&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitAudioContext&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;oscillator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&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;analyser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnalyser&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;gainNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;gainNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// silent — user hears nothing&lt;/span&gt;
  &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gainNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;gainNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequencyBinCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFloatFrequencyData&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="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&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;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// sample of frequency data&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;Font enumeration&lt;/strong&gt; via measurement comparison (no direct font list API exists, so it's inferred):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectFonts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testFonts&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;baseFonts&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;monospace&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;sans-serif&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;serif&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;testString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mmmmmmmmmmlli&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;testSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;72px&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;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;testSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;testString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&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;baseWidths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;baseFonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;font&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;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;baseWidths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&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;detected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;testFonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;font&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;return&lt;/span&gt; &lt;span class="nx"&gt;baseFonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&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;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt; &lt;span class="o"&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;font&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;base&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;baseWidths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&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;detected&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;
  
  
  Composite Scoring — How These Combine
&lt;/h2&gt;

&lt;p&gt;A single signal rarely identifies anyone uniquely. The entropy comes from combining them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_entropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal_distribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Shannon entropy in bits — higher = more identifying
    e.g. if 1 in 1000 users share your exact value, 
    that signal contributes ~10 bits of entropy
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal_distribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signal_distribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
        &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entropy&lt;/span&gt;

&lt;span class="c1"&gt;# Example combined fingerprint entropy
&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;screen_resolution&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# bits
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;canvas_hash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webgl_renderer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;6.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;font_list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timezone&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;audio_fingerprint&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;total_entropy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# ~32.3 bits
# 2^32.3 ≈ 5.3 billion possible combinations
# Global population ~8 billion — this fingerprint 
# alone approaches unique identification
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why the &lt;a href="https://coveryourtracks.eff.org/" rel="noopener noreferrer"&gt;EFF Panopticlick research&lt;/a&gt; consistently found most tested browsers had fingerprints unique among hundreds of thousands of samples — the combinatorial entropy adds up fast even when no individual signal is rare.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Reduces Entropy (Tested)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Firefox &lt;code&gt;privacy.resistFingerprinting&lt;/code&gt;&lt;/strong&gt; standardizes the highest-entropy signals:&lt;br&gt;
Testing before/after on the same machine with &lt;a href="https://amiunique.org/" rel="noopener noreferrer"&gt;amiunique.org&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;Signal&lt;/th&gt;
&lt;th&gt;Default Firefox&lt;/th&gt;
&lt;th&gt;resistFingerprinting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Canvas&lt;/td&gt;
&lt;td&gt;Unique hash&lt;/td&gt;
&lt;td&gt;Blocked/randomized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timezone&lt;/td&gt;
&lt;td&gt;Local (e.g. PST)&lt;/td&gt;
&lt;td&gt;UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen resolution&lt;/td&gt;
&lt;td&gt;Exact (e.g. 1512x982)&lt;/td&gt;
&lt;td&gt;Rounded (1500x950)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fonts detected&lt;/td&gt;
&lt;td&gt;40+ system fonts&lt;/td&gt;
&lt;td&gt;~12 bundled fonts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebGL renderer&lt;/td&gt;
&lt;td&gt;Full GPU string&lt;/td&gt;
&lt;td&gt;Generic string&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tradeoff: sites relying on accurate viewport dimensions for layout can render incorrectly, and some canvas-dependent web apps (image editors, games) break entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brave's fingerprinting protection&lt;/strong&gt; takes a different approach — randomizing per-session rather than blocking:&lt;br&gt;
&lt;strong&gt;Tor Browser&lt;/strong&gt; standardizes nearly everything to a single shared profile across all users, which is the only approach that achieves near-zero fingerprint uniqueness — at the cost of significant performance and compatibility overhead.&lt;/p&gt;


&lt;h2&gt;
  
  
  Detection-Side: If You're Building Anti-Fraud Systems
&lt;/h2&gt;

&lt;p&gt;For legitimate use cases (fraud detection, not tracking users for ads), fingerprinting libraries like FingerprintJS provide production-ready implementations:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FingerprintJS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@fingerprintjs/fingerprintjs&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;fpPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FingerprintJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fp&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;fpPromise&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;result&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;fp&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;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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visitorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stable identifier&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0-1 reliability&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting: privacy-focused browsers and extensions specifically target known fingerprinting libraries, so production fraud detection systems increasingly see degraded signal quality from privacy-conscious users — which itself becomes a (weaker) signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Cookie-based privacy controls (clearing cookies, incognito mode, cookie blockers) have zero effect on any of the above. If you're building privacy-respecting infrastructure or just hardening your own setup, the signals that matter are canvas, WebGL, audio context, and font enumeration — and the only consistently effective countermeasures are resistFingerprinting-style signal normalization or full standardization (Tor).&lt;/p&gt;

&lt;p&gt;Consumer-level explanation without the code: &lt;a href="https://lucas8.com/incognito-mode-browser-fingerprinting" rel="noopener noreferrer"&gt;lucas8.com/incognito-mode-browser-fingerprinting&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
